From 9c22c3bf54fb2174fbc0d2b1683db3a4183ba17b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:05:05 +0000 Subject: [PATCH 1/8] feat(migrate): a strict migration refers to no OpenSysML library Under -strict the migrator writes nothing a tool shipping only the standard library cannot resolve: the MigrationMetadata name markers become comments, a choice or junction a plain state, a branch probability a comment, an interval wait its midpoint, Java's whole-number quotient and ceiling the standard floor, a view carries no DiagramLayout geometry, and a history pseudostate, a document, a table or a Monte Carlo analysis is refused with a report line. The deferral encoding keeps its MigrationMetadata annotations. The default migration is unchanged. Co-Authored-By: jason.han --- ...rict-migration-standard-library.changed.md | 1 + cmd/sysml/usage.go | 2 +- docs/guide/03-command-line.md | 2 +- docs/reference/sysml-v1-migration.md | 60 +++++++---- internal/translate/migrate/activity.go | 23 +++- internal/translate/migrate/diagrams.go | 5 +- .../migrate/diagrams_internal_test.go | 9 +- internal/translate/migrate/documents.go | 6 ++ internal/translate/migrate/expression.go | 2 + internal/translate/migrate/interaction.go | 5 + internal/translate/migrate/migrate.go | 13 ++- internal/translate/migrate/montecarlo.go | 7 +- internal/translate/migrate/names.go | 13 ++- internal/translate/migrate/opaque.go | 24 ++++- internal/translate/migrate/simconfig.go | 10 +- internal/translate/migrate/states.go | 93 +++++++++++----- internal/translate/migrate/tables.go | 4 + internal/translate/migrate/translate.go | 4 + packaging/man/man1/sysml.1 | 3 +- tests/migrate/behavior_more_test.go | 46 ++++++-- tests/migrate/behavior_test.go | 4 +- tests/migrate/states_test.go | 101 ++++++++++++++---- 22 files changed, 337 insertions(+), 100 deletions(-) create mode 100644 changes/unreleased/strict-migration-standard-library.changed.md diff --git a/changes/unreleased/strict-migration-standard-library.changed.md b/changes/unreleased/strict-migration-standard-library.changed.md new file mode 100644 index 0000000000..de31b4e5de --- /dev/null +++ b/changes/unreleased/strict-migration-standard-library.changed.md @@ -0,0 +1 @@ +- **A `-strict` SysML v1 migration refers to no OpenSysML library.** The output loads in a tool that ships only the standard library — the pilot implementation, or a modeling tool built on it — where it used to fail on `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries` and `DiagramLayout` references. Made-up name markers become comments, a choice or junction a plain state, a branch probability a comment, an interval wait its midpoint, a view carries no layout, and a history pseudostate, a document, a table or a Monte Carlo analysis is refused with a report line; the deferral encoding keeps its `MigrationMetadata::Deferred*` annotations. The default migration is unchanged. diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go index 51abc40e69..4b1b5da6cc 100644 --- a/cmd/sysml/usage.go +++ b/cmd/sysml/usage.go @@ -649,7 +649,7 @@ func registerFlags(fs *flag.FlagSet) { fs.Var(&disabledLints, "disable-lint", "Leave this lint out of the model's diagnostics: undeclared-signal, port-type-mismatch, deferred-keeper-unmarked or rounded-real-literal, comma-separated or repeated") fs.Var(&enabledLints, "enable-lint", "Report this opt-in lint, off by default, in the model's diagnostics: rounded-real-literal, comma-separated or repeated; -disable-lint wins") fs.BoolVar(&noRecordCache, "no-record-cache", false, "Parse every file loaded and hold it loaded, reading no interface record from the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0") - fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it") + fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it and refers to no OpenSysML library") fs.Var(&modelChecks.constraints, "constraint", "Evaluate this constraint and exit (repeatable)") fs.BoolVar(&modelChecks.selfCheck, "self-check", false, "Apply the SysMLValidation constraints to every reflectively classified model element and exit") fs.Var(&modelChecks.selfCheckPackages, "self-check-package", "Apply this package's constraint defs with the SysMLValidation ones, as -self-check-package Acme::ModelingRules; implies -self-check (repeatable)") diff --git a/docs/guide/03-command-line.md b/docs/guide/03-command-line.md index a0eddcb438..d117f49df5 100644 --- a/docs/guide/03-command-line.md +++ b/docs/guide/03-command-line.md @@ -226,7 +226,7 @@ friends — reported as warnings that name the replacement, so a model that uses still analyses cleanly. `-strict` promotes those warnings to errors, which turns the run into a test of whether the file is conforming SysML v2. The flag applies to `-migrate` too: a strict SysML v1 migration writes no extension notation -at all — see [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict). +at all and refers to no OpenSysML library — see [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict). The state machine below uses the `choice` extension so the difference is visible: diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 1ac8bd2cc1..a23ef65862 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -1979,21 +1979,39 @@ without a model — is refused with an error naming the reason rather than migra ### Portable output with `-strict` A migration under `-strict` writes only notation a pinned SysML v2 production admits, so the -output analyses clean under [strict conformance](../guide/03-command-line.md#strict-conformance) -and carries nothing an interchange partner could not read. The `choice`, `junction`, -`shallowHistory` and `deepHistory` pseudostates are written -`#StateMachines:: state x;` — qualified so a member named like the metadata cannot -shadow it — with `private import StateMachines::*;` added to each package holding one, -under `-strict` the same as by default, and a `deferrableTrigger` is written in the -standard notation described under [Deferred signals](#deferred-signals) in both modes, -its deferred signal named by the `@MigrationMetadata::DeferredEvent` annotation. What a -strict migration still refuses is whatever has no standard v2 form at all; a transition to -or from an unmapped vertex is refused rather than written to an undeclared name. - -To check strict output against the pinned pilot implementation, validate it together with the -whole OpenSysML library directory, `.kerml` files included (`RandomFunctions`, which migrated -Monte Carlo analyses call, is KerML); see -[Pilot differential](../project/pilot-differential.md#the-kerml-side-of-the-bridge). +output analyses clean under [strict conformance](../guide/03-command-line.md#strict-conformance), +and refers to no library beyond the standard's, so a tool without OpenSysML's libraries — the +pilot implementation, or a modeling tool built on it — loads the file on its own. What the +default migration writes through an OpenSysML library a strict one writes otherwise, or refuses +with a report line: + +- the `MigrationMetadata::SynthesizedName`, `StandIn` and `LibraryNameAvoided` markers become + comments (`// names the migration made up: a, b`), the report carrying the same information; +- a `choice` or `junction` pseudostate, by default `#StateMachines:: state x;`, is a plain + `state x;` whose guarded transitions are the pseudostate's, reported approximated since a + run no longer passes through it in one step; a `shallowHistory` or `deepHistory` pseudostate, + which has no standard form, is refused, with the transitions into and out of it; +- a decision node's branch probability, by default `@Stochastic::Probability`, trails the + succession as a comment (`then b; // probability 0.25`); +- a duration constraint over an interval, by default a `RandomFunctions::uniform` draw, is a + fixed wait of the interval's midpoint when both bounds are literal numbers, refused otherwise; +- a «Document», a «DiagramTable» or «InstanceTable» and a Monte Carlo analysis, whose forms are + `DocumentQueries` and `Simulation::MonteCarlo`, are refused; a simulation configuration is + written with its run settings as a comment in place of `@Simulation::Configuration`; +- a view carries no `DiagramLayout` geometry, so `-layout` and the diagrams' own symbol streams + are not read. + +The one OpenSysML reference a strict migration keeps is the deferral encoding's +`@MigrationMetadata::DeferredEvent` and `#MigrationMetadata::DeferredKeeper` annotations, +described under [Deferred signals](#deferred-signals): the encoding itself is standard notation, +and the annotations are what lets a run give a transition on the deferred signal its due; a +tool that does not know them reads the state as it is written. A transition to or from an +unmapped vertex is refused rather than written to an undeclared name. + +Strict output validates against the pinned pilot implementation with the standard library +alone; see [Pilot differential](../project/pilot-differential.md). Default output needs the +whole OpenSysML library directory beside it, `.kerml` files included (`RandomFunctions`, which +migrated Monte Carlo analyses call, is KerML). #### Deferred signals @@ -2060,12 +2078,12 @@ transition the migration writes takes a signal, whatever it is written to — a transition into a terminate pseudostate, written to `done`, or into a submachine state through a connection point reference, written to the entry point's state, counts as any other: one it refuses — into a final state of another region, a state of -another machine, a vertex whose ends it refuses, or, under `-strict`, a pseudostate with no v2 -form such as a choice — takes none, so the deferral keeps every route it would have accepted -by; nor does such a completion transition drop the deferral. This is the one point where the -modes differ: the default migration writes a `choice` and the transition into it, so that -transition wins over the deferral as any other, while `-strict` refuses both and the state -keeps the signal. An internal transition of the +another machine, a vertex whose ends it refuses, or, under `-strict`, a history pseudostate, +which has no standard v2 form — takes none, so the deferral keeps every route it would have +accepted by; nor does such a completion transition drop the deferral. This is the one point +where the modes differ: the default migration writes the history pseudostate and the +transition into it, so that transition wins over the deferral as any other, while `-strict` +refuses both and the state keeps the signal. An internal transition of the state is written as a self transition, which exits and re-enters the state where v1 stayed in it: the exit action sends the kept occurrences to self, and the accept loop, started again by the re-entry, keeps them again unless a transition then accepts them — so the buffer survives diff --git a/internal/translate/migrate/activity.go b/internal/translate/migrate/activity.go index cc7e1c3121..3489eba26c 100644 --- a/internal/translate/migrate/activity.go +++ b/internal/translate/migrate/activity.go @@ -967,6 +967,11 @@ func (m *migration) waitOf(e *sysmlv1.Element) wait { case lo == hi: expr = lo note = joinNotes(note, "written as a fixed wait of "+lo+" s before "+describe(e)) + case m.strict && (lerr != nil || herr != nil): + return wait{dc: dc, why: noStrictDraw(lo, hi)} + case m.strict: + expr = computedLiteral((lf + hf) / 2) + note = joinNotes(note, "written as a fixed wait of "+expr+" s, the midpoint of ["+lo+", "+hi+"], before "+describe(e)+strictMidpointWhy) default: expr = "RandomFunctions::uniform(" + lo + ", " + hi + ")" note = joinNotes(note, "written as a wait drawn uniformly over ["+lo+", "+hi+"] s before "+describe(e)+"; a tool's fixed min or max mode is a run setting, not the model's") @@ -1101,16 +1106,30 @@ func (a *activity) decisionSuccessions(n *sysmlv1.Element, from string, outs []* } a.m.w.lines(guards[i].comment) tail := ";" - if weights != nil { + verdict, note := Mapped, "" + switch { + case weights == nil: + case a.m.strict: + tail = "; // probability " + weights[i] + verdict, note = Approximated, "its probability "+weights[i]+" is written as a comment: a strict migration names no OpenSysML library, and Stochastic::Probability is one" + default: tail = " { @Stochastic::Probability { p = " + weights[i] + "; } }" } a.succession([]*sysmlv1.Element{e}, from, guards[i].expr, to, tail) if guards[i].ok { - a.m.add(e, Mapped, a.m.edgeTarget(e), "") + a.m.add(e, verdict, a.m.edgeTarget(e), note) } } } +// strictMidpointWhy and noStrictDraw say why a strict migration writes a wait +// over an interval as its midpoint, or cannot. +const strictMidpointWhy = ": a strict migration names no OpenSysML library, and a draw over the interval is RandomFunctions::uniform" + +func noStrictDraw(lo, hi string) string { + return "a wait drawn over [" + lo + ", " + hi + "] is RandomFunctions::uniform, an OpenSysML library a strict migration does not name, and the bounds are no literal numbers to take the midpoint of" +} + // arbitraryChoice weights the unconditional branches of a decision equally, since // UML leaves the branch taken among several true guards unspecified. func (a *activity) arbitraryChoice(n *sysmlv1.Element, outs []*sysmlv1.Element, tos []string, guards []guardText, elseAt int) []string { diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 720e7d5a2b..0f071c65d7 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -539,6 +539,9 @@ func (m *migration) writeView(v *view) { } geo := m.viewGeometry(v, x, form) note = joinNotes(note, m.viewShownNote(d, x, geo, untyped)) + if m.strict && m.layoutSummary != nil { + note = joinNotes(note, "its layout is not written: a strict migration names no OpenSysML library, and DiagramLayout is one") + } doc, docNotes := m.proseNoted(d.Documentation) note = joinNotes(note, strings.Join(docNotes, "; ")) m.w.block(decl, func() { @@ -832,7 +835,7 @@ type viewGeometry struct { // stream carries. func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeometry { s := m.layoutSummary - if s == nil { + if s == nil || m.strict { return viewGeometry{} } rec, src := m.layoutRecord(v) diff --git a/internal/translate/migrate/diagrams_internal_test.go b/internal/translate/migrate/diagrams_internal_test.go index b93cd7e526..e706a592a2 100644 --- a/internal/translate/migrate/diagrams_internal_test.go +++ b/internal/translate/migrate/diagrams_internal_test.go @@ -328,7 +328,7 @@ func TestDiagramNotesDoNotNameOmittedVertices(t *testing.T) { strict bool anchored bool }{ - {name: "strict", strict: true, anchored: true}, + {name: "strict", strict: true}, {name: "non-strict", anchored: true}, } { t.Run(tc.name, func(t *testing.T) { @@ -348,6 +348,13 @@ func TestDiagramNotesDoNotNameOmittedVertices(t *testing.T) { } r := FromModelOptions("diagrams.xmi", model, Options{Strict: tc.strict}) got := string(r.Notation) + if tc.strict { + // A strict migration writes no layout, DiagramLayout being OpenSysML's. + if strings.Contains(got, "DiagramLayout") { + t.Errorf("strict notation names DiagramLayout:\n%s", got) + } + return + } anchored := strings.Contains(got, "metadata DiagramLayout::Note about") if anchored != tc.anchored { t.Errorf("note anchored = %t, want %t:\n%s", anchored, tc.anchored, got) diff --git a/internal/translate/migrate/documents.go b/internal/translate/migrate/documents.go index 8645dcd504..22a6a4e965 100644 --- a/internal/translate/migrate/documents.go +++ b/internal/translate/migrate/documents.go @@ -2814,6 +2814,12 @@ func (c *chain) dynamicView(s *sysmlv1.DocGenStep) { // writeDocument writes a planned document: its queries first, then the // Document definition holding its sections and blocks. func (m *migration) writeDocument(dp *docPlan) { + if m.strict { + note := "its v2 form is a Document of OpenSysML's DocumentQueries library, which a strict migration does not name" + m.w.lines(commentLines("not migrated: «Document» '" + dp.root.title + "' — " + note)) + m.report.Entries = append(m.report.Entries, *m.docEntry(dp.d, Unmapped, "", note)) + return + } m.writeQueries(dp.root, m.queryPrefix(dp.host), dp.host) var notes []string target := m.qualified(append(m.segments(dp.host), dp.root.name)) diff --git a/internal/translate/migrate/expression.go b/internal/translate/migrate/expression.go index a7e61dbf61..c892b97c84 100644 --- a/internal/translate/migrate/expression.go +++ b/internal/translate/migrate/expression.go @@ -50,6 +50,8 @@ type treeLowering struct { } // feature answers a placeholder for an instance operand, else asks the scope. +func (l *treeLowering) strictMigration() bool { return l.s.strictMigration() } + func (l *treeLowering) feature(path []string, write bool) (opaqueRef, *refusal) { if ref, ok := l.leaves[path[0]]; ok && len(path) == 1 { return ref, nil diff --git a/internal/translate/migrate/interaction.go b/internal/translate/migrate/interaction.go index 5347e58d93..0a47f8fc33 100644 --- a/internal/translate/migrate/interaction.go +++ b/internal/translate/migrate/interaction.go @@ -1623,6 +1623,11 @@ func (s *scenario) waitExpr(dc *sysmlv1.Element) (expr, note string, ok bool) { return "", "the interval's min " + lo + " exceeds its max " + hi, false case lo == hi: return lo, joinNotes(note, "a fixed wait of "+lo+" s"), true + case m.strict && (lerr != nil || herr != nil): + return "", noStrictDraw(lo, hi), false + case m.strict: + mid := computedLiteral((lf + hf) / 2) + return mid, joinNotes(note, "a fixed wait of "+mid+" s, the midpoint of ["+lo+", "+hi+"]"+strictMidpointWhy), true } return "RandomFunctions::uniform(" + lo + ", " + hi + ")", joinNotes(note, "a wait drawn uniformly over ["+lo+", "+hi+"] s; a tool's fixed min or max mode is a run setting, not the model's"), true diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 6824338ed3..107d53ddc9 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -60,10 +60,11 @@ type Options struct { // ImageBaseURL resolves a comment's relative to the server // serving it; "" leaves such images out. ImageBaseURL string - // Strict writes only notation a pinned SysML v2 production admits: a - // construct whose only v2 form is an OpenSysML extension (a deferred - // event, a choice, junction or history pseudostate) is reported unmapped - // instead of written. + // Strict writes only notation a pinned SysML v2 production admits and + // refers to no OpenSysML library: a construct whose only v2 form is one + // (a history pseudostate, a document) is reported unmapped instead of + // written, one with a standard approximation (a choice, a probability) + // written so and reported approximated. Strict bool } @@ -863,6 +864,10 @@ func (m *migration) libraryNameNotes() { prefix = "$::" } for _, r := range list { + if m.strict { + m.w.line("// " + writeName(r.fresh) + " is written for " + stringLiteral(r.src) + ", a name of the standard library") + continue + } m.w.line("metadata " + prefix + "MigrationMetadata::LibraryNameAvoided about " + writeName(r.fresh) + " { sourceName = " + stringLiteral(r.src) + "; }") } diff --git a/internal/translate/migrate/montecarlo.go b/internal/translate/migrate/montecarlo.go index 7313e2ad05..1a487a086d 100644 --- a/internal/translate/migrate/montecarlo.go +++ b/internal/translate/migrate/montecarlo.go @@ -221,7 +221,7 @@ func (m *migration) generalizesMonteCarlo(c *sysmlv1.Element) bool { // monteCarloCaseOf is the analysis def written beside a block: one that generalizes // the tool's MonteCarloAnalysis, or binds a statistic of one it inherits; nil for others. func (m *migration) monteCarloCaseOf(block *sysmlv1.Element) *monteCarloCase { - if block == nil || block.IsProxy() { + if block == nil || block.IsProxy() || m.strict { return nil } if cs, ok := m.monteCarlo[block]; ok { @@ -426,7 +426,10 @@ func (cs *monteCarloCase) statistics() []string { // MonteCarloAnalysis is written: as the sibling analysis def. func (m *migration) monteCarloGeneralization(block *sysmlv1.Element) string { cs := m.monteCarloCaseOf(block) - if cs == nil { + switch { + case cs == nil && m.strict: + return "generalization of the simulation tool's " + monteCarloAnalysisBlock + " is not written: its v2 form is an analysis def :> " + monteCarloLibraryCase + ", an OpenSysML library a strict migration does not name" + case cs == nil: return "generalization of the simulation tool's " + monteCarloAnalysisBlock + " is not written: the classifier is written as no part def for an analysis to take as its subject" } return "generalization of the simulation tool's " + monteCarloAnalysisBlock + " is written as the analysis def " + writeName(cs.name) + " :> " + monteCarloLibraryCase + " beside the part def, which is its subject" diff --git a/internal/translate/migrate/names.go b/internal/translate/migrate/names.go index 0127cd7d9f..4411b128e6 100644 --- a/internal/translate/migrate/names.go +++ b/internal/translate/migrate/names.go @@ -81,9 +81,20 @@ func (m *migration) standInNames(written []string) string { return m.migrationMarker(standInFQN, written) } +// strictMarkerText words the comment a strict migration writes instead of +// each MigrationMetadata marker, the library being OpenSysML's, not the standard's. +var strictMarkerText = map[string]string{ + synthesizedNameFQN: "names the migration made up", + standInFQN: "members standing for no source element", +} + // migrationMarker is the metadata usage of the MigrationMetadata definition -// fqn about the written names, qualified past a member shadowing the library. +// fqn about the written names, qualified past a member shadowing the library; +// a strict migration writes a comment instead. func (m *migration) migrationMarker(fqn string, written []string) string { + if m.strict { + return "// " + strictMarkerText[fqn] + ": " + strings.Join(written, ", ") + } prefix := "" if m.shadowsLibrary("MigrationMetadata", m.scope) { prefix = "$::" diff --git a/internal/translate/migrate/opaque.go b/internal/translate/migrate/opaque.go index 67228b7ef9..e72a4a0dac 100644 --- a/internal/translate/migrate/opaque.go +++ b/internal/translate/migrate/opaque.go @@ -747,6 +747,7 @@ type opaqueParser struct { i int d dialect kerml bool // a lowered Expression tree may spell the KerML connectives xor and implies + strict bool // write the standard library's functions only, never OpenSysMLMathFunctions sc featureResolver locals map[string]local // names a `var`, `let` or `const` declared assigns bool // whether `=` assigns (a statement) rather than compares @@ -771,7 +772,16 @@ func newOpaqueParser(body string, d dialect, sc featureResolver) (*opaqueParser, if err != nil { return nil, err } - return &opaqueParser{toks: toks, d: d, sc: sc, locals: map[string]local{}, assigned: map[string]bool{}, guarded: map[string][]string{}}, nil + p := &opaqueParser{toks: toks, d: d, sc: sc, locals: map[string]local{}, assigned: map[string]bool{}, guarded: map[string][]string{}} + if s, ok := sc.(strictTeller); ok { + p.strict = s.strictMigration() + } + return p, nil +} + +// strictTeller is a featureResolver telling whether the migration is strict. +type strictTeller interface { + strictMigration() bool } // local is a name a declaration introduced: the scalar it holds and whether @@ -1504,7 +1514,7 @@ func (p *opaqueParser) arithmeticOf(left translated, op string, right translated if op == "/" && p.d == dialectJava { switch { case wholeScalar(left.scalar) && wholeScalar(right.scalar): - return javaQuotient(left, right), nil + return javaQuotient(left, right, p.strict), nil case !realScalar(left.scalar) && !realScalar(right.scalar): side := left if side.scalar != "" { @@ -1520,7 +1530,12 @@ func (p *opaqueParser) arithmeticOf(left translated, op string, right translated // javaQuotient writes Java's `/` over whole numbers x and y: the exact quotient // truncated toward zero, which the extension library's quotient computes and // reports as overflow for the one pair (the least Integer by -1) outside the range. -func javaQuotient(x, y translated) translated { +// A strict migration computes it from the standard remainder, x % y having +// the sign of x: (x - x % y) / y is exact, and its floor is itself. +func javaQuotient(x, y translated, strict bool) translated { + if strict { + return translated{expr: "RealFunctions::floor((" + x.operand() + " - " + x.operand() + " % " + y.operand() + ") / " + y.operand() + ")", scalar: "Integer", atomic: true} + } return translated{expr: "OpenSysMLMathFunctions::quotient(" + x.expr + ", " + y.expr + ")", scalar: "Integer", atomic: true} } @@ -1816,6 +1831,9 @@ func (p *opaqueParser) mathRoundish(fn string, args []translated, arity func(int switch fn { case mathCeilFn: // -floor(-x) would overflow at the least Integer; the extension library's ceiling does not. + if p.strict { + return translated{expr: "-RealFunctions::floor(-" + args[0].operand() + ")", scalar: yields, atomic: true}, nil + } return translated{expr: "OpenSysMLMathFunctions::ceiling(" + args[0].expr + ")", scalar: yields, atomic: true}, nil case mathRound: // JavaScript and Java round a half toward +∞, where RealFunctions::round rounds it away from zero. diff --git a/internal/translate/migrate/simconfig.go b/internal/translate/migrate/simconfig.go index 6313e06a82..ccfd922c07 100644 --- a/internal/translate/migrate/simconfig.go +++ b/internal/translate/migrate/simconfig.go @@ -144,12 +144,20 @@ func (m *migration) simulationConfig(e *sysmlv1.Element, header, note string) { results.Notes = append(results.Notes, lost...) notes = append(notes, lost...) note = joinNotes(note, strings.Join(notes, "; ")) + if m.strict { + note = joinNotes(note, "its run settings are written as a comment: a strict migration names no OpenSysML library, and Simulation::Configuration is one's") + } m.add(e, verdictFor(note), m.v2Name(e), note) m.w.block(header, func() { saved := m.scope m.scope = e m.comments(e) - m.w.block("@Simulation::Configuration", func() { m.w.lines(settings.lines) }) + if m.strict { + m.w.lines(commentLines("simulation run settings, a comment since their v2 form is an OpenSysML library's: " + + strings.ReplaceAll(strings.Join(settings.lines, " "), "Simulation::DrawPolicy::", ""))) + } else { + m.w.block("@Simulation::Configuration", func() { m.w.lines(settings.lines) }) + } if target.element != nil { part := m.freshName(e, "target") results.Target = part diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index b2b06658ac..4d54659243 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -33,18 +33,42 @@ func theTransition(t *sysmlv1.Element) string { return "the transition " + descr func deferredBy(v *sysmlv1.Element) string { return "deferred by " + describe(v) } -// pseudostateLine spells the member a pseudostate of kind kw writes as: a -// StateMachines metadata usage for choice/junction/history — qualified so a -// member named like the metadata cannot shadow it — the standard literal for -// fork/join. -func pseudostateLine(prefix, kw, name string) string { +// pseudostateLine spells the member a pseudostate of kind kw writes as in +// scope: a StateMachines metadata usage for choice/junction/history — qualified +// so a member named like the metadata cannot shadow it, importing the library — +// the standard literal for fork/join. A strict migration, naming no OpenSysML +// library, writes a choice or junction as a plain state. +func (m *migration) pseudostateLine(scope *sysmlv1.Element, kw, name string) string { switch kw { case "choice", "junction", "shallowHistory", "deepHistory": - return "#" + prefix + "StateMachines::" + kw + " state " + name + ";" + if m.strict { + return "state " + name + ";" + } + m.useStateMachines() + return "#" + m.stateMachinesPrefix(scope) + "StateMachines::" + kw + " state " + name + ";" } return kw + " " + name + ";" } +// junctionSpelling says how a junction is written, for a report note. +func (m *migration) junctionSpelling() string { + if m.strict { + return "plain `state` standing for a junction, a strict migration naming no OpenSysML library," + } + return "`#StateMachines::junction state`" +} + +// historyWords says what each history pseudostate re-enters, for a report note. +var historyWords = map[string]string{ + "shallowHistory": "shallow history, which re-enters the substate active when its state was last left", + "deepHistory": "deep history, which re-enters the innermost states active when its state was last left", +} + +// noStrictHistory says why a strict migration refuses a history pseudostate. +func noStrictHistory(what string) string { + return what + " has no standard v2 form: a history pseudostate is written only through the OpenSysML StateMachines library, which a strict migration does not name" +} + // stateMachinesPrefix is "$::" when a member written as `StateMachines` hides // the library package from the scope a clause is written in, so the metadata // reference names it from the root. @@ -59,6 +83,9 @@ func (m *migration) stateMachinesPrefix(scope *sysmlv1.Element) string { // innermost package enclosing the current scope, once, for the StateMachines // metadata member being written. func (m *migration) useStateMachines() { + if m.strict { + return + } for e := m.scope; e != nil; e = e.Parent { switch e.Type { case "Package", "Model", "Profile": @@ -353,7 +380,7 @@ func (m *migration) entryPointForm(v, owner *sysmlv1.Element) pointForm { } regions := m.regionsCrossed(out, owner, "target") if len(out) < 2 || len(regions) < 2 { - return pointForm{kw: "junction", note: "written as a `#StateMachines::junction state` of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction"} + return pointForm{kw: "junction", note: "written as a " + m.junctionSpelling() + " of its state; a transition entering through it runs the state's entry behavior, then the transition leaving the junction"} } if len(regions) < len(out) { return pointForm{why: "the entry point starts several regions of " + describe(owner) + ", as a fork does, but two of its outgoing transitions enter the same region"} @@ -416,7 +443,7 @@ func (m *migration) exitPointForm(v, owner *sysmlv1.Element) pointForm { } regions := m.regionsCrossed(in, owner, "source") if len(in) < 2 || len(regions) < 2 { - return pointForm{kw: "junction", note: "written as a `#StateMachines::junction state` of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it"} + return pointForm{kw: "junction", note: "written as a " + m.junctionSpelling() + " of its state; a transition leaving through it runs the transition into the junction, the state's exit behavior, then the transition leaving it"} } joinBut := "several regions of " + describe(owner) + " leave through the exit point, as through a join, but " if len(regions) < len(in) { @@ -1118,28 +1145,29 @@ func (s *stateRegion) vertex(v *sysmlv1.Element) { switch pseudoKind(v) { case "initial": case "choice", "junction": - name := writeName(s.name(v)) - s.m.useStateMachines() - s.m.w.line(pseudostateLine(s.m.stateMachinesPrefix(v.Parent), pseudoKind(v), name)) + name, kind := writeName(s.name(v)), pseudoKind(v) + s.m.w.line(s.m.pseudostateLine(v.Parent, kind, name)) s.m.madeUp(v, name) - s.m.add(v, Mapped, name, "written as a #StateMachines::"+pseudoKind(v)+" state pseudostate, whose guarded transitions the runtime reads when it is reached; an unguarded one is its else branch") + if s.m.strict { + s.m.add(v, Approximated, name, "written as a plain state standing for the "+kind+" pseudostate, since a strict migration names no OpenSysML library and #StateMachines::"+kind+" is one's; its guarded transitions are the pseudostate's, but a runtime no longer passes through it in one step") + } else { + s.m.add(v, Mapped, name, "written as a #StateMachines::"+kind+" state pseudostate, whose guarded transitions the runtime reads when it is reached; an unguarded one is its else branch") + } case "fork", "join": name := writeName(s.name(v)) s.m.w.line(pseudoKind(v) + " " + name + ";") s.m.madeUp(v, name) s.m.add(v, Mapped, name, "written as a "+pseudoKind(v)+" pseudostate, whose segments the runtime fires together") - case "shallowHistory": - name := writeName(s.name(v)) - s.m.useStateMachines() - s.m.w.line(pseudostateLine(s.m.stateMachinesPrefix(v.Parent), "shallowHistory", name)) - s.m.madeUp(v, name) - s.m.add(v, Mapped, name, "written as a `#StateMachines::shallowHistory state` shallow history, which re-enters the substate active when its state was last left") - case "deepHistory": + case "shallowHistory", "deepHistory": + kind := pseudoKind(v) + if s.m.strict { + s.m.unmapped(v, noStrictHistory("the "+kind+" pseudostate")) + return + } name := writeName(s.name(v)) - s.m.useStateMachines() - s.m.w.line(pseudostateLine(s.m.stateMachinesPrefix(v.Parent), "deepHistory", name)) + s.m.w.line(s.m.pseudostateLine(v.Parent, kind, name)) s.m.madeUp(v, name) - s.m.add(v, Mapped, name, "written as a `#StateMachines::deepHistory state` deep history, which re-enters the innermost states active when its state was last left") + s.m.add(v, Mapped, name, "written as a `#StateMachines::"+kind+" state` "+historyWords[kind]) case "terminate": s.m.add(v, Approximated, "done", "a terminate pseudostate ends the machine; a transition to it is written to done, which ends its region") case "entryPoint", "exitPoint": @@ -1188,10 +1216,7 @@ func (m *migration) statePoints(v *sysmlv1.Element) int { m.add(cp, Mapped, m.vertexNames[v], f.note) default: name := writeName(m.vertexNames[cp]) - if f.kw == "junction" { - m.useStateMachines() - } - m.w.line(pseudostateLine(m.stateMachinesPrefix(m.scope), f.kw, name)) + m.w.line(m.pseudostateLine(m.scope, f.kw, name)) m.madeUp(cp, name) m.add(cp, Mapped, name, f.note) written++ @@ -2391,7 +2416,13 @@ func (s *stateRegion) target(t, v *sysmlv1.Element) (string, bool) { return p, ok } return s.endpoint(t, v, "target") - case "choice", "junction", "shallowHistory", "deepHistory": + case "shallowHistory", "deepHistory": + if s.m.strict { + s.m.add(t, Unmapped, "", noStrictHistory("its target "+describe(v))) + return "", false + } + return s.endpoint(t, v, "target") + case "choice", "junction": return s.endpoint(t, v, "target") case "fork", "join": return s.endpoint(t, v, "target") @@ -2418,7 +2449,13 @@ func (s *stateRegion) source(t, v *sysmlv1.Element) (string, bool) { return "", false } return s.endpoint(t, v, "source") - case "choice", "junction", "shallowHistory", "deepHistory": + case "shallowHistory", "deepHistory": + if s.m.strict { + s.m.add(t, Unmapped, "", noStrictHistory("its source "+describe(v))) + return "", false + } + return s.endpoint(t, v, "source") + case "choice", "junction": return s.endpoint(t, v, "source") case "fork", "join": return s.endpoint(t, v, "source") diff --git a/internal/translate/migrate/tables.go b/internal/translate/migrate/tables.go index 9b646d3d65..1335bfc387 100644 --- a/internal/translate/migrate/tables.go +++ b/internal/translate/migrate/tables.go @@ -201,6 +201,10 @@ func (m *migration) lowerTable(td *tableDoc) { t, host := td.t, td.v.host l := &lowered{} td.l = l + if m.strict { + l.refuse("its v2 form is a document of OpenSysML's DocumentQueries library, which a strict migration does not name") + return + } if len(t.Malformed) > 0 { l.refuse(strings.Join(t.Malformed, "; ")) } diff --git a/internal/translate/migrate/translate.go b/internal/translate/migrate/translate.go index e6db49bece..2c5a61f220 100644 --- a/internal/translate/migrate/translate.go +++ b/internal/translate/migrate/translate.go @@ -89,6 +89,8 @@ func (m *migration) lanesAround(e *sysmlv1.Element) (*lanes, *sysmlv1.Element) { // first, then what the scope sees, each further step a feature of the last. // The result is plural once any object the name reads through is, and a // write through a collection is refused: it would reach several objects. +func (s *bodyScope) strictMigration() bool { return s.m.strict } + func (s *bodyScope) feature(path []string, write bool) (opaqueRef, *refusal) { m := s.m full := strings.Join(path, ".") @@ -325,6 +327,8 @@ type nameReads struct { names []string } +func (r *nameReads) strictMigration() bool { return r.s.strictMigration() } + func (r *nameReads) feature(path []string, write bool) (opaqueRef, *refusal) { if r.s.m.pinCalled(r.s.scope, path[0]) != nil { return opaqueRef{expr: strings.Join(path, ".")}, nil diff --git a/packaging/man/man1/sysml.1 b/packaging/man/man1/sysml.1 index 53fb8c3625..3c0a48cc62 100644 --- a/packaging/man/man1/sysml.1 +++ b/packaging/man/man1/sysml.1 @@ -39,7 +39,8 @@ Report the model's diagnostics and exit, nonzero on an error; .TP .B \-strict Judge the model as conforming SysML v2: notation no pinned production admits -is an error, not a warning; a SysML v1 migration writes none of it +is an error, not a warning; a SysML v1 migration writes none of it and refers +to no OpenSysML library .TP .BR \-disable\-lint " \fIcode\fP" Leave this lint out of the model's diagnostics: undeclared\-signal, diff --git a/tests/migrate/behavior_more_test.go b/tests/migrate/behavior_more_test.go index b28907cbdd..581a9828a6 100644 --- a/tests/migrate/behavior_more_test.go +++ b/tests/migrate/behavior_more_test.go @@ -174,11 +174,14 @@ func TestStateMachineWithOrthogonalRegionsAndGuards(t *testing.T) { wantNote(t, r, "_dNoon", migrate.Unmapped, "only a signal event can be deferred, not a TimeEvent") // Strict output carries the deferral in the same standard notation; the - // choice pseudostate is written as metadata in both modes. + // choice pseudostate is written as a plain state, StateMachines being + // OpenSysML's library. strict := migrateDocumentOptions(t, ovenMachine, ovenApplications, migrate.Options{Strict: true}) wantDeferredDoorEncoding(t, strict) wantNote(t, strict, "_dNoon", migrate.Unmapped, "only a signal event can be deferred, not a TimeEvent") - wantNote(t, strict, "_pick", migrate.Mapped, "written as a #StateMachines::choice state pseudostate") + wantNote(t, strict, "_pick", migrate.Approximated, "written as a plain state standing for the choice pseudostate") + wantLine(t, strict.Notation, "state choice;") + wantNoLine(t, strict.Notation, "StateMachines::choice") wantNoLine(t, strict.Notation, "choice choice;") noExtensionStatement(t, strict.Notation) wantNote(t, strict, "_gWorn", migrate.Mapped, "") @@ -259,14 +262,14 @@ func wantDeferredDoorEncoding(t *testing.T, r *migrate.Result) { "#MigrationMetadata::DeferredKeeper action receive accept kept : Door;", "then action keep { assign deferred := SequenceFunctions::including(deferred, receive.kept); }", "then receive;", - "metadata MigrationMetadata::SynthesizedName about receive, keep;", "exit action flush {", "for kept in deferred { send kept to self; }", "then action clear { assign deferred := (); }", - "metadata MigrationMetadata::SynthesizedName about deferred, buffer, flush;", } { wantLine(t, r.Notation, line) } + wantMadeUp(t, r.Notation, "receive, keep") + wantMadeUp(t, r.Notation, "deferred, buffer, flush") wantNote(t, r, "_dDoor", migrate.Approximated, "kept in the item deferred by the accept loop of the do action buffer while the state is active, and sent to self by the exit action flush: the standard SysML v2 encoding of a deferred signal, which the state's @MigrationMetadata::DeferredEvent annotation records") wantNote(t, r, "_doorEv", migrate.Approximated, "deferred by 'Off' through the standard SysML v2 encoding, an accept loop keeping the signal while the state is active and an exit action sending it to self") } @@ -363,8 +366,8 @@ func TestStrictDeferredSignalsAreKeptAndReplayed(t *testing.T) { checkDeferredSignalsAreKeptAndReplayed(t, migrateDocumentOptions(t, deferringMachine, deferringApplications, mode.opts)) }) } - if d, s := migrateDocument(t, deferringMachine, deferringApplications), migrateDocumentOptions(t, deferringMachine, deferringApplications, migrate.Options{Strict: true}); string(d.Notation) != string(s.Notation) { - t.Errorf("default and strict migrations of the deferring machine differ:\n--- default\n%s\n--- strict\n%s", d.Notation, s.Notation) + if d, s := migrateDocument(t, deferringMachine, deferringApplications), migrateDocumentOptions(t, deferringMachine, deferringApplications, migrate.Options{Strict: true}); withoutNameMarkers(d.Notation) != withoutNameMarkers(s.Notation) { + t.Errorf("default and strict migrations of the deferring machine differ past the made-up name markers:\n--- default\n%s\n--- strict\n%s", d.Notation, s.Notation) } } @@ -397,7 +400,6 @@ func checkDeferredSignalsAreKeptAndReplayed(t *testing.T, r *migrate.Result) { "#MigrationMetadata::DeferredKeeper action receiveBeep accept keptBeep : Beep;", "then action keepBeep { assign deferredBeep := SequenceFunctions::including(deferredBeep, receiveBeep.keptBeep); }", "then receiveBeep;", - "metadata MigrationMetadata::SynthesizedName about split, run, receiveAlarm, keepAlarm, receiveBeep, keepBeep;", "exit action flush {", "action leave {", "assign context.exits := context.exits + 1;", @@ -405,14 +407,15 @@ func checkDeferredSignalsAreKeptAndReplayed(t *testing.T, r *migrate.Result) { "then action clearAlarm { assign deferredAlarm := (); }", "then for keptBeep in deferredBeep { send keptBeep to self; }", "then action clearBeep { assign deferredBeep := (); }", - "metadata MigrationMetadata::SynthesizedName about clearAlarm, clearBeep;", - "metadata MigrationMetadata::SynthesizedName about deferredAlarm, deferredBeep, buffer, flush;", "transition first Waiting accept Go then Working;", "transition first Busy accept Alarm then Alarmed;", "transition first Working accept Beep then Done;", } { wantLine(t, r.Notation, line) } + wantMadeUp(t, r.Notation, "split, run, receiveAlarm, keepAlarm, receiveBeep, keepBeep") + wantMadeUp(t, r.Notation, "clearAlarm, clearBeep") + wantMadeUp(t, r.Notation, "deferredAlarm, deferredBeep, buffer, flush") wantNote(t, r, "_dGo", migrate.Approximated, "the transition (_tGo) out of the state accepts the signal, which in v1 takes precedence over deferring it, so the state does not keep it; its @MigrationMetadata::DeferredEvent annotation records the deferral") wantNoLine(t, r.Notation, "item deferredGo : Go[*] ordered;") wantNote(t, r, "_goDeferredEv", migrate.Approximated, "deferred by 'Waiting', which the state's @MigrationMetadata::DeferredEvent annotation records; the state does not keep the signal, the transition (_tGo) accepting it") @@ -1047,7 +1050,7 @@ func TestDeferralYieldsToChoiceTransitionOnlyWhereWritten(t *testing.T) { wantNoLine(t, strict.Notation, "choice pick;") wantNoLine(t, strict.Notation, "item deferred : Door[*] ordered;") for _, line := range []string{ - "#StateMachines::choice state pick;", + "state pick;", "transition first Off accept Door then pick;", "transition first pick then Idle;", "@MigrationMetadata::DeferredEvent { ref :>> signal : Door; }", @@ -2437,3 +2440,26 @@ func TestDeferralCountsTransitionsToUnnamedTargets(t *testing.T) { t.Errorf("Idle did not complete into the submachine state:\n%s", out) } } + +// withoutNameMarkers is the notation less the markers of made-up names, which +// a default migration writes as MigrationMetadata and a strict one as comments. +func withoutNameMarkers(notation []byte) string { + var kept []string + for _, line := range strings.Split(string(notation), "\n") { + if strings.Contains(line, "SynthesizedName about") || strings.Contains(line, "// names the migration made up") { + continue + } + kept = append(kept, line) + } + return strings.Join(kept, "\n") +} + +// wantMadeUp checks the marker of the made-up names, which a default migration +// writes as MigrationMetadata and a strict one as a comment. +func wantMadeUp(t *testing.T, notation []byte, names string) { + t.Helper() + got := string(notation) + if !strings.Contains(got, "metadata MigrationMetadata::SynthesizedName about "+names+";") && !strings.Contains(got, "// names the migration made up: "+names) { + t.Errorf("notation marks no made-up names %q:\n%s", names, got) + } +} diff --git a/tests/migrate/behavior_test.go b/tests/migrate/behavior_test.go index 750aa1b1cf..351b40cca1 100644 --- a/tests/migrate/behavior_test.go +++ b/tests/migrate/behavior_test.go @@ -458,8 +458,8 @@ func TestPropertyBackedProbabilitiesAreReferences(t *testing.T) { func TestStrictPropertyProbabilityUsesOnlyLiteralDefaults(t *testing.T) { r := migrateFixtureFileOptions(t, "decision_property_probability", migrate.Options{Strict: true}) for _, line := range []string{ - "first 'with default' then defaulted { @Stochastic::Probability { p = 0.25; } }", - "first 'with default' then remainder { @Stochastic::Probability { p = 0.75; } }", + "first 'with default' then defaulted; // probability 0.25", + "first 'with default' then remainder; // probability 0.75", } { wantLine(t, r.Notation, line) } diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index 00269b9041..ce29d26eff 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -2,6 +2,8 @@ package migrate_test import ( "fmt" + "os" + "path/filepath" "strings" "testing" @@ -1627,50 +1629,49 @@ func noExtensionStatement(t *testing.T, notation []byte) { func TestStrictMigrationWritesNoExtensionNotation(t *testing.T) { r := migrateFixtureFileOptions(t, "plant_states", migrate.Options{Strict: true}) noExtensionStatement(t, r.Notation) + noOpenSysMLLibrary(t, r.Notation) for _, line := range []string{ - "private import StateMachines::*;", - "#StateMachines::junction state route;", - "#StateMachines::shallowHistory state last;", - "#StateMachines::deepHistory state deepest;", + "state route;", "fork spread;", "join gather;", } { wantLine(t, r.Notation, line) } - wantNote(t, r, "_junc", migrate.Mapped, "written as a #StateMachines::junction state pseudostate") - wantNote(t, r, "_hist", migrate.Mapped, "written as a `#StateMachines::shallowHistory state` shallow history") - wantNote(t, r, "_deep", migrate.Mapped, "written as a `#StateMachines::deepHistory state` deep history") - for _, id := range []string{"_tRoute", "_tBusy", "_tSpent", "_tResume", "_tHist"} { + wantNote(t, r, "_junc", migrate.Approximated, "written as a plain state standing for the junction pseudostate") + wantNote(t, r, "_hist", migrate.Unmapped, "the shallowHistory pseudostate has no standard v2 form") + wantNote(t, r, "_deep", migrate.Unmapped, "the deepHistory pseudostate has no standard v2 form") + for _, id := range []string{"_tRoute", "_tBusy", "_tSpent"} { wantNote(t, r, id, migrate.Mapped, "") } + wantNote(t, r, "_tResume", migrate.Unmapped, "its target 'last' has no standard v2 form") + wantNote(t, r, "_tHist", migrate.Unmapped, "its source 'last' has no standard v2 form") wantNote(t, r, "_gSpent", migrate.Mapped, "an else guard is written as the unguarded transition out of the junction") - wantNote(t, r, "_resumeEv", migrate.Mapped, "written where a trigger refers to it, as accept Resume") wantNote(t, r, "_fork", migrate.Mapped, "written as a fork pseudostate") wantNote(t, r, "_join", migrate.Mapped, "written as a join pseudostate") r = migrateFixtureFileOptions(t, "station_points", migrate.Options{Strict: true}) noExtensionStatement(t, r.Notation) + noOpenSysMLLibrary(t, r.Notation) for _, line := range []string{ - "private import StateMachines::*;", - "#StateMachines::junction state Start;", - "#StateMachines::junction state Leave;", - "#StateMachines::junction state Deep;", - "#StateMachines::junction state Out;", - "#StateMachines::shallowHistory state H;", + "state Start;", + "state Leave;", + "state Deep;", + "state Out;", } { wantLine(t, r.Notation, line) } - wantNote(t, r, "_start", migrate.Mapped, "written as a `#StateMachines::junction state` of its state; a transition entering through it runs the state's entry behavior") - wantNote(t, r, "_leave", migrate.Mapped, "written as a `#StateMachines::junction state` of its state; a transition leaving through it") - wantNote(t, r, "_deep", migrate.Mapped, "written as a `#StateMachines::junction state` of its state") - wantNote(t, r, "_out", migrate.Mapped, "written as a `#StateMachines::junction state` of its state") - wantNote(t, r, "_hist", migrate.Mapped, "written as a `#StateMachines::shallowHistory state` shallow history") + wantNote(t, r, "_start", migrate.Mapped, "written as a plain `state` standing for a junction, a strict migration naming no OpenSysML library, of its state; a transition entering through it runs the state's entry behavior") + wantNote(t, r, "_leave", migrate.Mapped, "written as a plain `state` standing for a junction, a strict migration naming no OpenSysML library, of its state; a transition leaving through it") + wantNote(t, r, "_deep", migrate.Mapped, "written as a plain `state` standing for a junction") + wantNote(t, r, "_out", migrate.Mapped, "written as a plain `state` standing for a junction") + wantNote(t, r, "_hist", migrate.Unmapped, "the shallowHistory pseudostate has no standard v2 form") wantNote(t, r, "_both", migrate.Mapped, "written as a fork of its state") wantNote(t, r, "_gather", migrate.Mapped, "written as a join of its state") wantNote(t, r, "_plain", migrate.Mapped, "no transition leaves the entry point") - for _, id := range []string{"_tGo", "_tDive", "_tDeep", "_tBack", "_tFinish", "_tLeave", "_tOut", "_tResume", "_tStart"} { + for _, id := range []string{"_tGo", "_tDive", "_tDeep", "_tBack", "_tFinish", "_tLeave", "_tOut", "_tStart"} { wantNote(t, r, id, migrate.Mapped, "") } + wantNote(t, r, "_tResume", migrate.Unmapped, "its target 'H' has no standard v2 form") // The pass-through route through 'Through' is still refused: it is not the // metadata spelling's doing but an unwritable route shape. wantNote(t, r, "_through", migrate.Unmapped, "leads from the entry point straight to the exit point") @@ -1724,3 +1725,61 @@ func TestStateMachinesPackageShadowsTheLibrary(t *testing.T) { wantLine(t, r.Notation, "private import $::StateMachines::*;") wantLine(t, r.Notation, "#$::StateMachines::choice state pick;") } + +// openSysMLLibraries names the library packages OpenSysML ships beyond the +// standard's, which a strict migration must not refer to. +func openSysMLLibraries(t *testing.T) []string { + t.Helper() + entries, err := os.ReadDir(filepath.Join("..", "..", "internal", "workspace", "libs", "stdlib", "OpenSysML Libraries")) + if err != nil { + t.Fatal(err) + } + var names []string + for _, e := range entries { + if name := strings.TrimSuffix(strings.TrimSuffix(e.Name(), ".sysml"), ".kerml"); name != e.Name() { + names = append(names, name) + } + } + if len(names) == 0 { + t.Fatal("no OpenSysML library found") + } + return names +} + +// noOpenSysMLLibrary fails on a line of notation referring to an OpenSysML +// library, past the deferral markers a strict migration keeps: they annotate +// standard notation the runtime reads them to schedule. +func noOpenSysMLLibrary(t *testing.T, notation []byte) { + t.Helper() + libs := openSysMLLibraries(t) + for _, line := range strings.Split(string(notation), "\n") { + if strings.Contains(line, "MigrationMetadata::DeferredEvent") || strings.Contains(line, "MigrationMetadata::DeferredKeeper") { + continue + } + for _, lib := range libs { + if strings.Contains(line, lib+"::") || strings.Contains(line, "import "+lib+";") { + t.Errorf("strict notation refers to the OpenSysML library %s:\n%s", lib, line) + } + } + } +} + +// TestStrictMigrationRefersToNoOpenSysMLLibrary migrates every fixture under +// strict and checks the notation names only the standard library, so a tool +// without OpenSysML's libraries loads it. +func TestStrictMigrationRefersToNoOpenSysMLLibrary(t *testing.T) { + fixtures, err := filepath.Glob(filepath.Join("testdata", "xmi", "*.xmi")) + if err != nil { + t.Fatal(err) + } + if len(fixtures) == 0 { + t.Fatal("no fixture found") + } + for _, f := range fixtures { + name := strings.TrimSuffix(filepath.Base(f), ".xmi") + t.Run(name, func(t *testing.T) { + r := migrateFixtureFileOptions(t, name, migrate.Options{Strict: true}) + noOpenSysMLLibrary(t, r.Notation) + }) + } +} From af562a2dd6812a6ac4dbd3b038d9de47e383d2ea Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:16:12 +0000 Subject: [PATCH 2/8] fix(migrate): a strict probability note reaches an edge whose guard is not migrated; docs say a Monte Carlo analysis is approximated Co-Authored-By: jason.han --- .../strict-migration-standard-library.changed.md | 2 +- docs/reference/sysml-v1-migration.md | 7 ++++--- internal/translate/migrate/activity.go | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/changes/unreleased/strict-migration-standard-library.changed.md b/changes/unreleased/strict-migration-standard-library.changed.md index de31b4e5de..97d11b6608 100644 --- a/changes/unreleased/strict-migration-standard-library.changed.md +++ b/changes/unreleased/strict-migration-standard-library.changed.md @@ -1 +1 @@ -- **A `-strict` SysML v1 migration refers to no OpenSysML library.** The output loads in a tool that ships only the standard library — the pilot implementation, or a modeling tool built on it — where it used to fail on `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries` and `DiagramLayout` references. Made-up name markers become comments, a choice or junction a plain state, a branch probability a comment, an interval wait its midpoint, a view carries no layout, and a history pseudostate, a document, a table or a Monte Carlo analysis is refused with a report line; the deferral encoding keeps its `MigrationMetadata::Deferred*` annotations. The default migration is unchanged. +- **A `-strict` SysML v1 migration refers to no OpenSysML library.** The output loads in a tool that ships only the standard library — the pilot implementation, or a modeling tool built on it — where it used to fail on `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries` and `DiagramLayout` references. Made-up name markers become comments, a choice or junction a plain state, a branch probability a comment, an interval wait its midpoint, a view carries no layout, a Monte Carlo analysis loses its `Simulation::MonteCarlo` generalization, and a history pseudostate, a document or a table is refused with a report line; the deferral encoding keeps its `MigrationMetadata::Deferred*` annotations. The default migration is unchanged. diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index a23ef65862..0b6ad7923b 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -1995,9 +1995,10 @@ with a report line: succession as a comment (`then b; // probability 0.25`); - a duration constraint over an interval, by default a `RandomFunctions::uniform` draw, is a fixed wait of the interval's midpoint when both bounds are literal numbers, refused otherwise; -- a «Document», a «DiagramTable» or «InstanceTable» and a Monte Carlo analysis, whose forms are - `DocumentQueries` and `Simulation::MonteCarlo`, are refused; a simulation configuration is - written with its run settings as a comment in place of `@Simulation::Configuration`; +- a «Document», a «DiagramTable» or «InstanceTable», whose forms are `DocumentQueries` + documents, are refused; a Monte Carlo analysis is written as the ordinary analysis without its + `Simulation::MonteCarlo` generalization, reported approximated, and a simulation configuration + with its run settings as a comment in place of `@Simulation::Configuration`; - a view carries no `DiagramLayout` geometry, so `-layout` and the diagrams' own symbol streams are not read. diff --git a/internal/translate/migrate/activity.go b/internal/translate/migrate/activity.go index 3489eba26c..25a261ae1a 100644 --- a/internal/translate/migrate/activity.go +++ b/internal/translate/migrate/activity.go @@ -1116,8 +1116,11 @@ func (a *activity) decisionSuccessions(n *sysmlv1.Element, from string, outs []* tail = " { @Stochastic::Probability { p = " + weights[i] + "; } }" } a.succession([]*sysmlv1.Element{e}, from, guards[i].expr, to, tail) - if guards[i].ok { + switch { + case guards[i].ok: a.m.add(e, verdict, a.m.edgeTarget(e), note) + case note != "": + a.m.note(e, note) } } } From 3f39a5909e6881a8d2231eb5890bb4f202abf601 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:16:45 +0000 Subject: [PATCH 3/8] test(migrate): a strict probability note reaches an edge whose guard is not migrated Co-Authored-By: jason.han --- tests/migrate/behavior_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/migrate/behavior_test.go b/tests/migrate/behavior_test.go index 351b40cca1..c86791fede 100644 --- a/tests/migrate/behavior_test.go +++ b/tests/migrate/behavior_test.go @@ -1018,3 +1018,10 @@ func TestNestedDefProbabilityReadsThroughContext(t *testing.T) { run(t, r) }) } + +func TestStrictProbabilityNoteReachesAnEdgeWhoseGuardIsNotMigrated(t *testing.T) { + r := migrateDocumentOptions(t, missionActivity, missionApplications, migrate.Options{Strict: true}) + wantNote(t, r, "_e8", migrate.Approximated, "its probability 0.25 is written as a comment") + wantNote(t, r, "_e9", migrate.Approximated, "its probability 0.75 is written as a comment") + wantNoLine(t, r.Notation, "Stochastic::Probability") +} From e31e28332edd616c5702fe4fe5183c5b5e3adced Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:10:50 +0000 Subject: [PATCH 4/8] fix(migrate): a strict else branch negates the choice's other guards; strict documents and layouts are accounted for A strict migration writes a choice or junction as a plain state, whose transitions compete, so its else branch is now guarded by the negation of the other outgoing guards, or written unguarded with a report line when one of them is not a v2 expression. A document a strict migration refuses is refused before its content is planned, so the report holds no mapped rows for views and paragraphs no Document carries. A strict migration still joins layout records to the views they match, counting the geometry it omits instead of reporting the views missing. Co-Authored-By: jason.han --- ...rict-migration-standard-library.changed.md | 2 +- docs/reference/sysml-v1-migration.md | 13 +++-- internal/translate/migrate/diagrams.go | 6 ++- internal/translate/migrate/documents.go | 34 +++++++++--- internal/translate/migrate/report.go | 22 ++++++-- internal/translate/migrate/states.go | 44 +++++++++++++++ tests/migrate/layout_test.go | 18 +++++++ tests/migrate/migrate_test.go | 15 ++++++ tests/migrate/states_test.go | 54 ++++++++++++++++++- 9 files changed, 190 insertions(+), 18 deletions(-) diff --git a/changes/unreleased/strict-migration-standard-library.changed.md b/changes/unreleased/strict-migration-standard-library.changed.md index 97d11b6608..d7310c28ac 100644 --- a/changes/unreleased/strict-migration-standard-library.changed.md +++ b/changes/unreleased/strict-migration-standard-library.changed.md @@ -1 +1 @@ -- **A `-strict` SysML v1 migration refers to no OpenSysML library.** The output loads in a tool that ships only the standard library — the pilot implementation, or a modeling tool built on it — where it used to fail on `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries` and `DiagramLayout` references. Made-up name markers become comments, a choice or junction a plain state, a branch probability a comment, an interval wait its midpoint, a view carries no layout, a Monte Carlo analysis loses its `Simulation::MonteCarlo` generalization, and a history pseudostate, a document or a table is refused with a report line; the deferral encoding keeps its `MigrationMetadata::Deferred*` annotations. The default migration is unchanged. +- **A `-strict` SysML v1 migration refers to no OpenSysML library.** The output loads in a tool that ships only the standard library — the pilot implementation, or a modeling tool built on it — where it used to fail on `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries` and `DiagramLayout` references. Made-up name markers become comments, a choice or junction a plain state whose else branch is guarded by the negation of the other guards, a branch probability a comment, an interval wait its midpoint, a view carries no layout, a Monte Carlo analysis loses its `Simulation::MonteCarlo` generalization, and a history pseudostate, a document (with every view and paragraph in it) or a table is refused with a report line; a layout export is still joined to the views, the report counting the geometry it omits; the deferral encoding keeps its `MigrationMetadata::Deferred*` annotations. The default migration is unchanged. diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 0b6ad7923b..3641208a8b 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -1989,18 +1989,23 @@ with a report line: comments (`// names the migration made up: a, b`), the report carrying the same information; - a `choice` or `junction` pseudostate, by default `#StateMachines:: state x;`, is a plain `state x;` whose guarded transitions are the pseudostate's, reported approximated since a - run no longer passes through it in one step; a `shallowHistory` or `deepHistory` pseudostate, + run no longer passes through it in one step; its `else` branch, which a plain state's + transitions would compete with rather than fall back to, is guarded by the negation of the + other guards (`if not (count < 2)`), or written unguarded with a report line saying so when + one of them is not a v2 expression; a `shallowHistory` or `deepHistory` pseudostate, which has no standard form, is refused, with the transitions into and out of it; - a decision node's branch probability, by default `@Stochastic::Probability`, trails the succession as a comment (`then b; // probability 0.25`); - a duration constraint over an interval, by default a `RandomFunctions::uniform` draw, is a fixed wait of the interval's midpoint when both bounds are literal numbers, refused otherwise; - a «Document», a «DiagramTable» or «InstanceTable», whose forms are `DocumentQueries` - documents, are refused; a Monte Carlo analysis is written as the ordinary analysis without its + documents, are refused, a «Document» before its content is planned, so every view and + paragraph in it is reported unmapped with it; a Monte Carlo analysis is written as the ordinary analysis without its `Simulation::MonteCarlo` generalization, reported approximated, and a simulation configuration with its run settings as a comment in place of `@Simulation::Configuration`; -- a view carries no `DiagramLayout` geometry, so `-layout` and the diagrams' own symbol streams - are not read. +- a view carries no `DiagramLayout` geometry; a `-layout` export and the diagrams' own symbol + streams are still joined to the views they match, the report's layout account counting the + views whose geometry is omitted rather than reporting them missing. The one OpenSysML reference a strict migration keeps is the deferral encoding's `@MigrationMetadata::DeferredEvent` and `#MigrationMetadata::DeferredKeeper` annotations, diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 0f071c65d7..ff9ef0472d 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -835,7 +835,7 @@ type viewGeometry struct { // stream carries. func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeometry { s := m.layoutSummary - if s == nil || m.strict { + if s == nil { return viewGeometry{} } rec, src := m.layoutRecord(v) @@ -854,6 +854,10 @@ func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeomet default: s.StreamDiagrams++ } + if m.strict { + s.GeometryOmitted++ + return viewGeometry{note: "layout from " + source + " omitted: a strict migration names no OpenSysML library, and DiagramLayout is one's"} + } prefix := diagramLayoutPrefix if m.shadowsLibrary("DiagramLayout", v.host) { prefix = globalDiagramLayoutPrefix diff --git a/internal/translate/migrate/documents.go b/internal/translate/migrate/documents.go index 22a6a4e965..8a1b2b9804 100644 --- a/internal/translate/migrate/documents.go +++ b/internal/translate/migrate/documents.go @@ -195,6 +195,10 @@ func (m *migration) planDocument(d *sysmlv1.DocGenDocument) { if title == "" { title = "Document" } + if m.strict { + m.strictDocument(d, host, title) + return + } dp := &docPlan{d: d, host: host, refused: map[string]bool{}} dp.root = §ionPlan{v: d.Root, title: title, names: columnNames{}} dp.root.name = m.viewName(host, title+docSuffix) @@ -203,6 +207,30 @@ func (m *migration) planDocument(d *sysmlv1.DocGenDocument) { m.extras[host] = append(m.extras[host], func() { m.writeDocument(dp) }) } +// strictDocument accounts for a document a strict migration does not write, +// before any of its content is planned: a comment where the Document would +// be, and an Unmapped entry for it and for every view and paragraph in it. +func (m *migration) strictDocument(d *sysmlv1.DocGenDocument, host *sysmlv1.Element, title string) { + note := "its v2 form is a Document of OpenSysML's DocumentQueries library, which a strict migration does not name" + m.extras[host] = append(m.extras[host], func() { + m.w.lines(commentLines("not migrated: «Document» '" + title + "' — " + note)) + }) + m.report.Entries = append(m.report.Entries, *m.docEntry(d, Unmapped, "", note)) + var walk func(v *sysmlv1.DocGenView) + walk = func(v *sysmlv1.DocGenView) { + if v.Class != d.Class { + m.report.Entries = append(m.report.Entries, *m.nodeEntry(v.Class, viewApplication(v.Class), Unmapped, "it belongs to «Document» '"+title+"', which is not migrated: "+note)) + } + for _, p := range v.Paragraphs { + m.report.Entries = append(m.report.Entries, *m.nodeEntry(p.Comment, p.Application, Unmapped, "it belongs to «Document» '"+title+"', which is not migrated: "+note)) + } + for _, c := range v.Children { + walk(c) + } + } + walk(d.Root) +} + // nameAnchors names the anchors after their definitions, clear of every // member name the document declares, so a chain from one resolves anywhere in it. func (m *migration) nameAnchors(dp *docPlan) { @@ -2814,12 +2842,6 @@ func (c *chain) dynamicView(s *sysmlv1.DocGenStep) { // writeDocument writes a planned document: its queries first, then the // Document definition holding its sections and blocks. func (m *migration) writeDocument(dp *docPlan) { - if m.strict { - note := "its v2 form is a Document of OpenSysML's DocumentQueries library, which a strict migration does not name" - m.w.lines(commentLines("not migrated: «Document» '" + dp.root.title + "' — " + note)) - m.report.Entries = append(m.report.Entries, *m.docEntry(dp.d, Unmapped, "", note)) - return - } m.writeQueries(dp.root, m.queryPrefix(dp.host), dp.host) var notes []string target := m.qualified(append(m.segments(dp.host), dp.root.name)) diff --git a/internal/translate/migrate/report.go b/internal/translate/migrate/report.go index c3969206cf..a9a4dcc5dc 100644 --- a/internal/translate/migrate/report.go +++ b/internal/translate/migrate/report.go @@ -103,6 +103,9 @@ type LayoutSummary struct { DiagramsUnmatched int `json:"diagramsUnmatched"` // ViewsWithoutLayout counts the written views the export does not cover. ViewsWithoutLayout int `json:"viewsWithoutLayout"` + // GeometryOmitted counts the joined or stream-laid-out views whose + // geometry a strict migration leaves unwritten, DiagramLayout being OpenSysML's. + GeometryOmitted int `json:"geometryOmitted,omitempty"` // Placements counts the shown elements the export positions; // PlacementsWritten those positioned into a view, PlacementsUnexposed // those the view neither exposes nor draws, PlacementsDangling those @@ -184,9 +187,13 @@ func (r *Report) Summary() string { total, c[Mapped], c[Approximated], c[Unmapped], c[Skipped]-unreferenced, unreferenced) if l := r.Layout; l != nil { laidOut := l.DiagramsJoined + l.StreamDiagrams - s += fmt.Sprintf("; laid out %d of %d diagrams from %s: %s elements positioned, %s connectors routed, %s styled, %s notes", - laidOut, laidOut+l.ViewsWithoutLayout, l.Source, - commas(l.PlacementsWritten), commas(l.RoutesWritten), commas(l.StylesWritten), commas(l.Notes)) + if l.GeometryOmitted > 0 { + s += fmt.Sprintf("; matched %d of %d diagrams to %s, whose geometry the strict migration omits", laidOut, laidOut+l.ViewsWithoutLayout, l.Source) + } else { + s += fmt.Sprintf("; laid out %d of %d diagrams from %s: %s elements positioned, %s connectors routed, %s styled, %s notes", + laidOut, laidOut+l.ViewsWithoutLayout, l.Source, + commas(l.PlacementsWritten), commas(l.RoutesWritten), commas(l.StylesWritten), commas(l.Notes)) + } } if r.Images > 0 { s += fmt.Sprintf("; wrote %d image file(s)", r.Images) @@ -277,10 +284,17 @@ func (l *LayoutSummary) writeText(b *strings.Builder) { } else { b.WriteString("# ") } - fmt.Fprintf(b, "%d views laid out from their own symbol stream, %d without layout\n", l.StreamDiagrams, l.ViewsWithoutLayout) + streamVerb := "laid out from" + if l.GeometryOmitted > 0 { + streamVerb = "matched to" + } + fmt.Fprintf(b, "%d views %s their own symbol stream, %d without layout\n", l.StreamDiagrams, streamVerb, l.ViewsWithoutLayout) if l.StreamSupplemented > 0 { fmt.Fprintf(b, "# %d joined views supplemented from their own symbol stream where the record placed or routed nothing\n", l.StreamSupplemented) } + if l.GeometryOmitted > 0 { + fmt.Fprintf(b, "# %d views' geometry omitted by the strict migration\n", l.GeometryOmitted) + } fmt.Fprintf(b, "# placements: %d of %d written (%d not exposed, %d resolving to no element); routes: %d of %d written (%d not pinned, %d resolving to no element); malformed: %d\n", l.PlacementsWritten, l.Placements, l.PlacementsUnexposed, l.PlacementsDangling, l.RoutesWritten, l.Routes, l.RoutesUnexposed, l.RoutesDangling, l.Malformed) diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index 4d54659243..9439b1ff90 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -3007,6 +3007,9 @@ func (s *stateRegion) guard(t, src *sysmlv1.Element) (string, string) { return "", "" } if k := pseudoKind(src); (k == "choice" || k == "junction") && isElseGuard(spec) { + if s.m.strict { + return s.strictElse(g, t, src, k) + } s.m.add(g, Mapped, "", "an else guard is written as the unguarded transition out of the "+k+", which is taken when no guarded one holds") return "", "" } @@ -3020,6 +3023,47 @@ func (s *stateRegion) guard(t, src *sysmlv1.Element) (string, string) { return " if " + expr, note } +// strictElse writes an else guard out of a choice or junction a strict migration +// writes as a plain state, whose transitions compete rather than yield to a +// holding guard: the negation of the other written transitions' guards when +// each is a v2 expression, else unguarded with a note on what that risks. +func (s *stateRegion) strictElse(g, t, src *sysmlv1.Element, kind string) (string, string) { + var others []string + for _, o := range s.m.outgoing[src] { + if o == t || !s.transitionWritten(o) { + continue + } + og := s.m.guardOf(o) + spec := firstOwned(og, "specification") + switch { + case og == nil, spec == nil, trueLiteral(spec): + others = append(others, "true") + continue + case isElseGuard(spec): + continue + } + expr, ok, _ := s.m.behaviorValue(spec, o) + if !ok { + note := "the else guard is written as an unguarded transition, since the guard [" + describeValue(spec) + "] of the transition " + describe(o) + " is not a v2 expression to negate; the " + kind + " is a plain state whose transitions compete, so this branch may be taken while that guard holds" + s.m.add(g, Approximated, "", note) + return "", note + } + others = append(others, expr) + } + if len(others) == 0 { + s.m.add(g, Mapped, "", "an else guard beside no guarded transition out of the "+kind+" is not written") + return "", "" + } + for i, o := range others { + if len(others) > 1 { + others[i] = "(" + o + ")" + } + } + note := "the else guard is written as the negation of the other guards out of the " + kind + ", a plain state whose transitions compete rather than fall back to the else branch" + s.m.add(g, Approximated, "", note) + return " if not (" + strings.Join(others, " or ") + ")", note +} + // isElseGuard reports whether a guard's specification is the word else, which // a v1 tool writes on the branch a choice takes when no other guard holds. func isElseGuard(spec *sysmlv1.Element) bool { diff --git a/tests/migrate/layout_test.go b/tests/migrate/layout_test.go index d5c97ab2d4..40d69f56c3 100644 --- a/tests/migrate/layout_test.go +++ b/tests/migrate/layout_test.go @@ -115,6 +115,24 @@ func TestGoldenLayout(t *testing.T) { for _, d := range errorsMode(t, "layout.sysml", strict.Notation, diag.ConformanceStrict) { t.Errorf("%v", d) } + // The strict migration joins the same records to the same views; only + // their geometry is left unwritten, which the summary says instead of + // reporting the views as missing. + l, sl := r.Report.Layout, strict.Report.Layout + if sl.DiagramsJoined != l.DiagramsJoined || sl.DiagramsUnmatched != l.DiagramsUnmatched || sl.ViewsWithoutLayout != l.ViewsWithoutLayout || sl.GeometryOmitted != l.DiagramsJoined || sl.PlacementsWritten != 0 { + t.Errorf("strict layout summary: %+v\nwant joined %d, unmatched %d, without %d, all geometry omitted", sl, l.DiagramsJoined, l.DiagramsUnmatched, l.ViewsWithoutLayout) + } + if l.GeometryOmitted != 0 { + t.Errorf("default layout summary omits geometry: %+v", l) + } + for _, e := range strict.Report.Entries { + if e.Kind == "Layout" && strings.Contains(e.Note, "does not write as a view") { + t.Errorf("strict migration reports a joined view missing: %+v", e) + } + } + if !strings.Contains(strict.Report.Summary(), "geometry the strict migration omits") { + t.Errorf("strict summary = %q", strict.Report.Summary()) + } } func TestGoldenLayoutExposedMetadataAttribute(t *testing.T) { diff --git a/tests/migrate/migrate_test.go b/tests/migrate/migrate_test.go index 94d039855e..ef2a65a406 100644 --- a/tests/migrate/migrate_test.go +++ b/tests/migrate/migrate_test.go @@ -438,6 +438,21 @@ func TestGoldenConstructFixtures(t *testing.T) { // A strict migration writes only notation a pinned SysML v2 production admits: // every fixture's strict output analyses with zero error diagnostics in strict // conformance mode, where extension notation is an error. +// A strict migration refuses a DocGen document before planning its content, +// so the report holds an Unmapped row for the document and each view and +// paragraph in it, and no Mapped row for content no Document holds. +func TestStrictMigrationRefusesDocumentsBeforePlanningThem(t *testing.T) { + r := migrateFixtureFileOptions(t, "montecarlo_docgen_scoped", migrate.Options{Strict: true}) + wantLine(t, r.Notation, "/* not migrated: «Document» 'Statistics Report' — its v2 form is a Document of OpenSysML's DocumentQueries library, which a strict migration does not name */") + if strings.Contains(string(r.Notation), "DocumentQueries::") { + t.Errorf("strict notation names DocumentQueries:\n%s", r.Notation) + } + wantNote(t, r, "_s8", migrate.Unmapped, "its v2 form is a Document of OpenSysML's DocumentQueries library") + for _, id := range []string{"_s4", "_s5"} { + wantNote(t, r, id, migrate.Unmapped, "it belongs to «Document» 'Statistics Report', which is not migrated") + } +} + func TestStrictMigrationAnalysesCleanUnderStrictConformance(t *testing.T) { for _, name := range append([]string{"vehicle"}, constructFixtures...) { t.Run(name, func(t *testing.T) { diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index ce29d26eff..257102f4d9 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -1634,18 +1634,21 @@ func TestStrictMigrationWritesNoExtensionNotation(t *testing.T) { "state route;", "fork spread;", "join gather;", + "transition first route if context.count < 2 then Work;", + "transition first route if not (context.count < 2) then done;", } { wantLine(t, r.Notation, line) } wantNote(t, r, "_junc", migrate.Approximated, "written as a plain state standing for the junction pseudostate") wantNote(t, r, "_hist", migrate.Unmapped, "the shallowHistory pseudostate has no standard v2 form") wantNote(t, r, "_deep", migrate.Unmapped, "the deepHistory pseudostate has no standard v2 form") - for _, id := range []string{"_tRoute", "_tBusy", "_tSpent"} { + for _, id := range []string{"_tRoute", "_tBusy"} { wantNote(t, r, id, migrate.Mapped, "") } wantNote(t, r, "_tResume", migrate.Unmapped, "its target 'last' has no standard v2 form") wantNote(t, r, "_tHist", migrate.Unmapped, "its source 'last' has no standard v2 form") - wantNote(t, r, "_gSpent", migrate.Mapped, "an else guard is written as the unguarded transition out of the junction") + wantNote(t, r, "_tSpent", migrate.Approximated, "the else guard is written as the negation of the other guards out of the junction") + wantNote(t, r, "_gSpent", migrate.Approximated, "the else guard is written as the negation of the other guards out of the junction, a plain state whose transitions compete rather than fall back to the else branch") wantNote(t, r, "_fork", migrate.Mapped, "written as a fork pseudostate") wantNote(t, r, "_join", migrate.Mapped, "written as a join pseudostate") @@ -1783,3 +1786,50 @@ func TestStrictMigrationRefersToNoOpenSysMLLibrary(t *testing.T) { }) } } + +// A choice whose other guard is not a v2 expression leaves a strict +// migration nothing to negate: the else branch is written unguarded and the +// report says it may be taken while that guard holds. +const opaqueChoiceMachine = ` + + + + + + + + + + + + + + + Focus lost + + + + + else + + + + + ` + +func TestStrictChoiceElseBesideAnUnmigratedGuardIsReported(t *testing.T) { + r := migrateDocumentOptions(t, opaqueChoiceMachine, "", migrate.Options{Strict: true}) + noOpenSysMLLibrary(t, r.Notation) + for _, line := range []string{ + "state pick;", + "/* guard not migrated: [Focus lost] — not v2 expression syntax */", + "transition first pick then A;", + "transition first pick then B;", + } { + wantLine(t, r.Notation, line) + } + wantNote(t, r, "_ogB", migrate.Approximated, "the else guard is written as an unguarded transition, since the guard [Focus lost] of the transition (_otA) is not a v2 expression to negate; the choice is a plain state whose transitions compete, so this branch may be taken while that guard holds") + + r = migrateDocument(t, opaqueChoiceMachine, "") + wantNote(t, r, "_ogB", migrate.Mapped, "an else guard is written as the unguarded transition out of the choice") +} From 6903ad005d5965494d8e3f526fd0fbddb3a2dbed Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:55:17 +0000 Subject: [PATCH 5/8] fix(migrate): a strict else negates only the guards of transitions strict mode writes; omitted geometry counts no stream supplementation Co-Authored-By: jason.han --- internal/translate/migrate/diagrams.go | 7 ++-- internal/translate/migrate/states.go | 4 +- tests/migrate/states_test.go | 52 ++++++++++++++++++++++++++ tests/migrate/streams_test.go | 24 ++++++++++++ 4 files changed, 82 insertions(+), 5 deletions(-) diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index ff9ef0472d..798b3460f9 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -539,9 +539,6 @@ func (m *migration) writeView(v *view) { } geo := m.viewGeometry(v, x, form) note = joinNotes(note, m.viewShownNote(d, x, geo, untyped)) - if m.strict && m.layoutSummary != nil { - note = joinNotes(note, "its layout is not written: a strict migration names no OpenSysML library, and DiagramLayout is one") - } doc, docNotes := m.proseNoted(d.Documentation) note = joinNotes(note, strings.Join(docNotes, "; ")) m.w.block(decl, func() { @@ -848,7 +845,7 @@ func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeomet case src.export: s.DiagramsJoined++ m.layoutJoined[rec.ID] = true - if src.stream { + if src.stream && !m.strict { s.StreamSupplemented++ } default: @@ -856,6 +853,8 @@ func (m *migration) viewGeometry(v *view, x exposures, form viewForm) viewGeomet } if m.strict { s.GeometryOmitted++ + // Nothing is written, so the stream supplements nothing. + source = m.layoutSourceName(layoutSources{export: src.export}) return viewGeometry{note: "layout from " + source + " omitted: a strict migration names no OpenSysML library, and DiagramLayout is one's"} } prefix := diagramLayoutPrefix diff --git a/internal/translate/migrate/states.go b/internal/translate/migrate/states.go index 9439b1ff90..cc9f824a52 100644 --- a/internal/translate/migrate/states.go +++ b/internal/translate/migrate/states.go @@ -1902,7 +1902,9 @@ func (s *stateRegion) transitionWritten(t *sysmlv1.Element) bool { return named(pointOwner(tgt)) } return named(tgt) - case "choice", "junction", "shallowHistory", "deepHistory", "fork", "join": + case "shallowHistory", "deepHistory": + return !s.m.strict && named(tgt) + case "choice", "junction", "fork", "join": return named(tgt) } case "ConnectionPointReference": diff --git a/tests/migrate/states_test.go b/tests/migrate/states_test.go index 257102f4d9..70b351da45 100644 --- a/tests/migrate/states_test.go +++ b/tests/migrate/states_test.go @@ -1817,6 +1817,58 @@ const opaqueChoiceMachine = ` ` +// historyChoiceMachine is a choice whose guarded branch targets a shallow +// history pseudostate, which a strict migration refuses, beside an else branch. +const historyChoiceMachine = ` + + + + + + + + + + + + + + + + + + + + n < 2 + + + + + else + + + + + ` + +// A strict else guard negates only the guards of branches the strict migration +// writes: one into a history pseudostate is refused, so it does not block the fallback. +func TestStrictChoiceElseIgnoresRefusedBranches(t *testing.T) { + r := migrateDocumentOptions(t, historyChoiceMachine, "", migrate.Options{Strict: true}) + noOpenSysMLLibrary(t, r.Notation) + wantLine(t, r.Notation, "state pick;") + wantLine(t, r.Notation, "transition first pick then B;") + wantNoLine(t, r.Notation, "if not") + if es := entriesFor(r, "_htA"); len(es) != 1 || es[0].Verdict != migrate.Unmapped { + t.Errorf("_htA entries = %+v, want unmapped", es) + } + wantNote(t, r, "_hgB", migrate.Mapped, "an else guard beside no guarded transition out of the choice is not written") + + r = migrateDocument(t, historyChoiceMachine, "") + wantLine(t, r.Notation, "#StateMachines::shallowHistory state back;") + wantNote(t, r, "_hgB", migrate.Mapped, "an else guard is written as the unguarded transition out of the choice") +} + func TestStrictChoiceElseBesideAnUnmigratedGuardIsReported(t *testing.T) { r := migrateDocumentOptions(t, opaqueChoiceMachine, "", migrate.Options{Strict: true}) noOpenSysMLLibrary(t, r.Notation) diff --git a/tests/migrate/streams_test.go b/tests/migrate/streams_test.go index ef2c9c4a22..9480777186 100644 --- a/tests/migrate/streams_test.go +++ b/tests/migrate/streams_test.go @@ -362,6 +362,30 @@ func TestExportPrecedesStream(t *testing.T) { "_diag_modes", "laid out from modes.layout.xml supplemented by the diagram's own symbol stream: 2 of 3 shown elements positioned") } +// A strict migration joins the export's record to its view but writes no +// geometry, so the report counts the omission, not a supplementation. +func TestStrictExportIsJoinedButNotSupplemented(t *testing.T) { + layout, err := mtip.Parse([]byte(modesExport)) + if err != nil { + t.Fatalf("mtip.Parse: %v", err) + } + r, err := migrate.MigrateOptions("figures.mdzip", mdzip(t, figureStreams), + migrate.Options{Layout: layout, LayoutSource: "modes.layout.xml", Strict: true}) + if err != nil { + t.Fatalf("MigrateOptions: %v", err) + } + wantNoLine(t, r.Notation, "DiagramLayout") + s := r.Report.Layout + if s == nil || s.DiagramsJoined != 1 || s.StreamDiagrams != 6 || s.StreamSupplemented != 0 || s.GeometryOmitted != 7 { + t.Fatalf("layout summary = %+v; want 1 joined, 6 from streams, none supplemented, 7 omitted", s) + } + report := reportText(t, r) + if strings.Contains(report, "supplemented") { + t.Errorf("the strict report claims a supplementation it did not write:\n%s", report) + } + wantInOrder(t, "omission", report, "_diag_modes", "layout from modes.layout.xml omitted") +} + // An MTIP export placing and routing everything the Pump Modes stream draws // leaves the stream nothing to supplement, yet its frame still sizes the canvas. func TestFrameSurvivesCompleteExport(t *testing.T) { From eeb1bf9a813f5a8b35ce553d63228051183017d0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:28:04 +0000 Subject: [PATCH 6/8] feat(migrate): -portable appends the OpenSysML library packages the output refers to A migration under -portable is one self-contained file: every OpenSysML library package its notation names by qualified name, and every one those refer to in turn, follows the model as a library package (a KerML library in its SysML spelling), so the file loads where only the standard library ships. What the migration writes is otherwise unchanged; the report names the packages inlined. Co-Authored-By: jason.han --- ...table-migration-inlines-libraries.added.md | 1 + cmd/sysml/main.go | 1 + cmd/sysml/migrate.go | 4 +- cmd/sysml/usage.go | 2 + docs/guide/03-command-line.md | 2 +- docs/guide/11-migrating-from-sysml-v1.md | 51 +++-- docs/reference/cli.md | 1 + docs/reference/sysml-v1-migration.md | 22 ++- internal/translate/migrate/migrate.go | 10 +- internal/translate/migrate/portable.go | 181 ++++++++++++++++++ internal/translate/migrate/portable_test.go | 46 +++++ internal/translate/migrate/report.go | 13 ++ internal/translate/migrate/results.go | 2 +- packaging/man/man1/sysml.1 | 5 + tests/migrate/portable_test.go | 83 ++++++++ 15 files changed, 400 insertions(+), 24 deletions(-) create mode 100644 changes/unreleased/portable-migration-inlines-libraries.added.md create mode 100644 internal/translate/migrate/portable.go create mode 100644 internal/translate/migrate/portable_test.go create mode 100644 tests/migrate/portable_test.go diff --git a/changes/unreleased/portable-migration-inlines-libraries.added.md b/changes/unreleased/portable-migration-inlines-libraries.added.md new file mode 100644 index 0000000000..5808c58f01 --- /dev/null +++ b/changes/unreleased/portable-migration-inlines-libraries.added.md @@ -0,0 +1 @@ +- **`-portable` makes a SysML v1 migration one self-contained file.** With `-migrate sysml -portable`, the OpenSysML library packages the output refers to — `DiagramLayout`, `MigrationMetadata`, `StateMachines`, `Stochastic`, `DocumentQueries`, and whatever those refer to in turn — are appended after the model, a KerML library in its SysML spelling, so the file loads in a tool that ships only the standard library without changing what the migration writes; the report names the packages inlined. `-strict` remains the way to an output that refers to no OpenSysML library at all. diff --git a/cmd/sysml/main.go b/cmd/sysml/main.go index e088b5bc86..0739c5d355 100644 --- a/cmd/sysml/main.go +++ b/cmd/sysml/main.go @@ -152,6 +152,7 @@ var ( htmlMath string htmlTheme string strictMode bool + portableMode bool disabledLints lintList enabledLints lintList noRecordCache bool diff --git a/cmd/sysml/migrate.go b/cmd/sysml/migrate.go index c708046109..29af7ee872 100644 --- a/cmd/sysml/migrate.go +++ b/cmd/sysml/migrate.go @@ -139,7 +139,7 @@ func migrateInput(name string, data []byte, _ convert.Format, to convert.Format) // options; none were given when the flag was not passed. func migrationOptions() (convert.MigrateOptions, error) { if layoutPath == "" { - return convert.MigrateOptions{ImageBaseURL: imageBaseURL, Strict: strictMode}, nil + return convert.MigrateOptions{ImageBaseURL: imageBaseURL, Strict: strictMode, Portable: portableMode}, nil } data, err := os.ReadFile(layoutPath) if err != nil { @@ -149,7 +149,7 @@ func migrationOptions() (convert.MigrateOptions, error) { if err != nil { return convert.MigrateOptions{}, fmt.Errorf("%s: %w", layoutPath, err) } - return convert.MigrateOptions{Layout: layout, LayoutSource: layoutPath, ImageBaseURL: imageBaseURL, Strict: strictMode}, nil + return convert.MigrateOptions{Layout: layout, LayoutSource: layoutPath, ImageBaseURL: imageBaseURL, Strict: strictMode, Portable: portableMode}, nil } // writeMigrationReport writes the report to the -migration-report file (JSON when diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go index 4b1b5da6cc..d93c38759a 100644 --- a/cmd/sysml/usage.go +++ b/cmd/sysml/usage.go @@ -649,6 +649,7 @@ func registerFlags(fs *flag.FlagSet) { fs.Var(&disabledLints, "disable-lint", "Leave this lint out of the model's diagnostics: undeclared-signal, port-type-mismatch, deferred-keeper-unmarked or rounded-real-literal, comma-separated or repeated") fs.Var(&enabledLints, "enable-lint", "Report this opt-in lint, off by default, in the model's diagnostics: rounded-real-literal, comma-separated or repeated; -disable-lint wins") fs.BoolVar(&noRecordCache, "no-record-cache", false, "Parse every file loaded and hold it loaded, reading no interface record from the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0") + fs.BoolVar(&portableMode, "portable", false, "With -migrate: append the OpenSysML library packages the output refers to, so the one file loads in a tool that ships only the standard library; the report names them") fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it and refers to no OpenSysML library") fs.Var(&modelChecks.constraints, "constraint", "Evaluate this constraint and exit (repeatable)") fs.BoolVar(&modelChecks.selfCheck, "self-check", false, "Apply the SysMLValidation constraints to every reflectively classified model element and exit") @@ -826,6 +827,7 @@ func optionGroups() []usage.OptionGroup { usage.Opt("migration-results", fileArg), usage.Opt("layout", fileArg), usage.Opt("image-base-url", ""), + usage.Opt("portable", ""), usage.Opt("compare-results", fileArg), }, }, { diff --git a/docs/guide/03-command-line.md b/docs/guide/03-command-line.md index d117f49df5..073507f213 100644 --- a/docs/guide/03-command-line.md +++ b/docs/guide/03-command-line.md @@ -226,7 +226,7 @@ friends — reported as warnings that name the replacement, so a model that uses still analyses cleanly. `-strict` promotes those warnings to errors, which turns the run into a test of whether the file is conforming SysML v2. The flag applies to `-migrate` too: a strict SysML v1 migration writes no extension notation -at all and refers to no OpenSysML library — see [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict). +at all and refers to no OpenSysML library — see [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict); `-portable` instead appends the OpenSysML library packages a default migration refers to, so the one file loads elsewhere unchanged. The state machine below uses the `choice` extension so the difference is visible: diff --git a/docs/guide/11-migrating-from-sysml-v1.md b/docs/guide/11-migrating-from-sysml-v1.md index 7c6b6c704d..34607fe499 100644 --- a/docs/guide/11-migrating-from-sysml-v1.md +++ b/docs/guide/11-migrating-from-sysml-v1.md @@ -315,24 +315,39 @@ its type: `end [0..1] driver : Driver;`. From this point `%save` in the REPL, `- LSP and the clients all take the file as they take any other; nothing remembers that it was migrated. -## Portable output with `-strict` - -Pseudostates are written through the `StateMachines` library's metadata -spellings — `#StateMachines::junction state x;` — with `private import -StateMachines::*;` added to each package that holds one, so a migrated model -reads as conforming SysML v2 either way. Pass `-strict` (see -[Strict conformance](03-command-line.md#strict-conformance)) and the migration -still writes only notation a pinned grammar admits — the pseudostates keep -their `StateMachines` metadata spellings in both modes: - -```console -$ sysml Project.xmi -strict -migrate sysml -o Project.sysml -$ sysml -strict -validate Project.sysml -✓ Project.sysml: no errors -``` - -What a strict migration refuses is described in the reference under -[Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict). +## Portable output: `-portable` and `-strict` + +A migrated model refers to OpenSysML's own library packages where v1 has no standard v2 +form: a pseudostate is written through the `StateMachines` library's metadata spellings +(`#StateMachines::junction state x;`, with `private import StateMachines::*;` added to each +package that holds one), a branch probability through `Stochastic`, a view's geometry through +`DiagramLayout`, the names the migration made up through `MigrationMetadata`. OpenSysML ships +them, so the file loads here as it is; a tool that ships only the standard library — the pilot +implementation, or a modeling tool built on it — reports every such reference unresolved. Two +flags make the output portable to such a tool: + +- `-portable` keeps the migration as it is and appends the library packages the output refers + to, so the one file carries everything it needs; the report names them: + + ```console + $ sysml Project.xmi -migrate sysml -portable -o Project.sysml + migration: … inlined 4 OpenSysML library package(s): DiagramLayout, MigrationMetadata, StateMachines, Stochastic + ``` + +- `-strict` (see [Strict conformance](03-command-line.md#strict-conformance)) writes no + OpenSysML reference at all — a pseudostate as a plain state, a probability as a comment, a + view without geometry — at the cost of what the reference lists under + [Portable output with `-strict`](../reference/sysml-v1-migration.md#portable-output-with--strict), + so the output also reads as conforming SysML v2: + + ```console + $ sysml Project.xmi -strict -migrate sysml -o Project.sysml + $ sysml -strict -validate Project.sysml + ✓ Project.sysml: no errors + ``` + +Both are described in the reference under +[Portable output with `-portable`](../reference/sysml-v1-migration.md#portable-output-with--portable). A deferred signal is standard notation in both modes: the state gets an `item` buffer its do action fills from an accept loop while the state is active, substates included, and its exit diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d59b7cdff8..8459356425 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -249,6 +249,7 @@ the same member-path parser as `Project` and `OrderBy`. | `--migration-report ` | | With `--migrate`: write the element-by-element migration report to this file, JSON when it ends in `.json`, text otherwise. Without it the one-line summary goes to stderr | | `--migration-results ` | | With `--migrate`: write the simulation tool's run configurations (`SimulationProfile:SimulationConfig`) and the result snapshots it stored for each of them to this JSON file — the sidecar `-compare-results` reads against the migrated model. See [Comparing a migrated configuration with the tool's results](#comparing-a-migrated-configuration-with-the-tools-results) | | `--image-base-url ` | | With `--migrate`: the absolute http(s) URL a comment's relative `` — a path the View Editor serves, such as `/projects/.../png` — is resolved against, so the migrated document's `Image` block points at the server instead of losing the image (see [SysML v1 migration](sysml-v1-migration.md)) | +| `--portable` | | With `--migrate`: append the OpenSysML library packages the output refers to, so the one file loads in a tool that ships only the standard library; the report names them (see [Portable output with `--portable`](sysml-v1-migration.md#portable-output-with--portable)) | | `--render ` | | Render this view of the model (every file named, loaded as one) instead of running it, in the form its `render` member states (see [Rendering a view](#rendering-a-view)) | | `--render-all ` | | Render every declared view into the directory, one artifact per view | | `--graphs ` | | Write the lowered graph of this action or state machine, and of every behavior it performs, as the canonical `graphs:1` JSON an external analysis engine is sent ([the `graphs:1` model form](external-engines.md#the-graphs1-model-form)) to stdout, or to `--output`. The subject is a qualified name, definition or usage; a name that is no behavior, or that nothing declares, is refused with exit status 2. Cannot be combined with checks, rendering, `--convert`, `--migrate`, `--query` or `--eval` | diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 3641208a8b..7bf43d8b11 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -38,7 +38,7 @@ sysml: Model.mdzip is a SysML v1 model, which is migrated, not converted: every ``` and `-migrate` refuses v2 input the same way, pointing at `-convert`. The companion flags — -`-migration-report`, `-migration-results`, `-layout`, `-image-base-url` and `-strict` — accompany +`-migration-report`, `-migration-results`, `-layout`, `-image-base-url`, `-portable` and `-strict` — accompany `-migrate`. The same migration is available over gRPC (`Migrate`, with `from_format: "xmi"`, `"uml"` or @@ -1976,6 +1976,26 @@ Every migrated model is gated in the test suite to: A model the reader cannot make sense of — not XMI, a zipped project container, a document without a model — is refused with an error naming the reason rather than migrated partially. +### Portable output with `-portable` + +The default migration writes what v1 has no standard v2 form for through OpenSysML's own +library packages — `StateMachines` for a pseudostate, `Stochastic` for a branch probability, +`DiagramLayout` for a view's geometry, `MigrationMetadata` for the names the migration made up, +`DocumentQueries` for a document or table, `RandomFunctions` or `Simulation` for a simulation +profile — which OpenSysML ships and a tool built on the standard library alone does not. +`-portable` leaves the migration as it is and appends those packages to the output: every +library package the notation refers to by qualified name, and every one those refer to in +turn, follows the model under a comment saying so, written as the library ships it except +that a `standard library package` becomes a `library package`, which is what a file of one's +own may declare. A KerML library (`RandomFunctions`, `OpenSysMLMathFunctions`) is written in +its SysML spelling, its functions as `calc def`s; one with no SysML spelling is left +referenced, and the report says so. The report's summary names the packages inlined — +`inlined 5 OpenSysML library package(s): DiagramLayout, DocumentQueries, MigrationMetadata, +StateMachines, Stochastic` — and its JSON form lists them under `libraries`. The one file then +loads in the pilot implementation, or a modeling tool built on it, on its own; OpenSysML loads +it too, the inlined packages standing in for its own. A migration that must also read as +standard notation, with no OpenSysML reference left in it, is `-strict`'s, below. + ### Portable output with `-strict` A migration under `-strict` writes only notation a pinned SysML v2 production admits, so the diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index 107d53ddc9..5c6446ecb3 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -66,6 +66,10 @@ type Options struct { // written, one with a standard approximation (a choice, a probability) // written so and reported approximated. Strict bool + // Portable appends the OpenSysML library packages the output refers to, + // so the one file loads in a tool that ships only the standard library; + // what is written is otherwise unchanged. The report names the packages. + Portable bool } // Migrate reads a SysML v1 model as UML XMI, or a zip archive (such as a @@ -271,7 +275,11 @@ func FromModelOptions(name string, model *sysmlv1.Model, opts Options) *Result { m.layoutReport() m.extensions() m.report.Images = m.imagesWritten - return &Result{Notation: []byte(m.w.String()), Report: m.report, Results: m.results, Files: m.files} + notation := []byte(m.w.String()) + if opts.Portable { + notation, m.report.Libraries = inlineLibraries(notation) + } + return &Result{Notation: notation, Report: m.report, Results: m.results, Files: m.files} } // unwrittenEvents reports the events whose triggers were never written: those diff --git a/internal/translate/migrate/portable.go b/internal/translate/migrate/portable.go new file mode 100644 index 0000000000..727d6945a0 --- /dev/null +++ b/internal/translate/migrate/portable.go @@ -0,0 +1,181 @@ +package migrate + +import ( + "bytes" + "sort" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/lexer" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" +) + +// openSysMLLibraryDir is the directory of the library source holding the +// OpenSysML extension libraries, one library package per file named after it. +const openSysMLLibraryDir = "OpenSysML Libraries/" + +// LibrarySummary accounts for the OpenSysML library packages a portable +// migration appended to its output, and those it could not. +type LibrarySummary struct { + // Inlined names the library packages appended, in the order appended. + Inlined []string `json:"inlined,omitempty"` + // NotInlined explains, by package, why a referenced library package was + // not appended, so the output still refers to it. + NotInlined map[string]string `json:"notInlined,omitempty"` +} + +// libraryText is one OpenSysML library package as the source ships it. +type libraryText struct { + name string + kerml bool + text []byte +} + +// openSysMLLibraries reads the OpenSysML extension libraries from the +// embedded library source, by package name. +func openSysMLLibraries() map[string]libraryText { + src := libs.EmbeddedSource() + out := map[string]libraryText{} + for _, path := range src.List() { + if !strings.HasPrefix(path, openSysMLLibraryDir) { + continue + } + base := strings.TrimPrefix(path, openSysMLLibraryDir) + name, kerml := strings.TrimSuffix(base, ".sysml"), false + if name == base { + name, kerml = strings.TrimSuffix(base, ".kerml"), true + if name == base { + continue + } + } + text, err := src.Read(path) + if err != nil { + continue + } + out[name] = libraryText{name: name, kerml: kerml, text: text} + } + return out +} + +// inlineLibraries appends to notation the OpenSysML library packages it +// refers to, and those they refer to in turn, so the file loads with the +// standard library alone. A KerML library is written in its SysML spelling; +// one that has none is left referenced and accounted for. +func inlineLibraries(notation []byte) ([]byte, *LibrarySummary) { + available := openSysMLLibraries() + summary := &LibrarySummary{} + seen := map[string]bool{} + var appended [][]byte + pending := referencedLibraries(notation, available) + for len(pending) > 0 { + name := pending[0] + pending = pending[1:] + if seen[name] { + continue + } + seen[name] = true + lib := available[name] + text := lib.text + if lib.kerml { + spelled, ok := sysmlSpelling(name, text) + if !ok { + if summary.NotInlined == nil { + summary.NotInlined = map[string]string{} + } + summary.NotInlined[name] = "a KerML library with no SysML spelling" + continue + } + text = spelled + } + text = unmarkedStandard(name, text) + summary.Inlined = append(summary.Inlined, name) + appended = append(appended, text) + pending = append(pending, referencedLibraries(text, available)...) + } + if len(appended) == 0 { + return notation, summary + } + var out bytes.Buffer + out.Write(notation) + if !bytes.HasSuffix(notation, []byte("\n")) { + out.WriteByte('\n') + } + out.WriteString("\n// OpenSysML library packages the model refers to, inlined so the file\n// loads with the standard library alone.\n") + for _, text := range appended { + out.WriteByte('\n') + out.Write(text) + if !bytes.HasSuffix(text, []byte("\n")) { + out.WriteByte('\n') + } + } + return out.Bytes(), summary +} + +// referencedLibraries names the OpenSysML library packages notation refers +// to, each as the first segment of a qualified name, sorted. +func referencedLibraries(notation []byte, available map[string]libraryText) []string { + found := map[string]bool{} + lx := lexer.New(source.New("migration.sysml", notation)) + var prev lexer.Token + for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() { + if tok.IsTrivia() { + continue + } + if tok.Kind == lexer.ColonColon && prev.Kind == lexer.Identifier { + if name := string(notation[prev.Span.Offset : prev.Span.Offset+prev.Span.Len]); available[name].text != nil { + found[name] = true + } + } + prev = tok + } + names := make([]string, 0, len(found)) + for name := range found { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// unmarkedStandard is a library package declared `standard library package` +// without the `standard`, which only the standard library's own files may carry. +func unmarkedStandard(name string, text []byte) []byte { + lx := lexer.New(source.New(name+".sysml", text)) + var prev lexer.Token + for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() { + if tok.IsTrivia() { + continue + } + if tok.Kind == lexer.Keyword && string(text[tok.Span.Offset:tok.Span.Offset+tok.Span.Len]) == "library" && prev.Kind == lexer.Keyword && string(text[prev.Span.Offset:prev.Span.Offset+prev.Span.Len]) == "standard" { + out := append([]byte(nil), text[:prev.Span.Offset]...) + return append(out, text[tok.Span.Offset:]...) + } + prev = tok + } + return text +} + +// sysmlSpelling writes a KerML library in SysML notation: its functions as +// calc defs. ok is false when a word of it is KerML's alone, as `feature` or +// `featured by` are, so no SysML file can say what it says. +func sysmlSpelling(name string, kerml []byte) (spelled []byte, ok bool) { + var out bytes.Buffer + lx := lexer.New(source.New(name+".kerml", kerml)) + pos := 0 + for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() { + if tok.Kind != lexer.Keyword { + continue + } + end := tok.Span.Offset + tok.Span.Len + word := string(kerml[tok.Span.Offset:end]) + switch { + case word == "function": + out.Write(kerml[pos:tok.Span.Offset]) + out.WriteString("calc def") + pos = end + case !source.IsKeywordIn(word, source.KindSysML): + return nil, false + } + } + out.Write(kerml[pos:]) + return out.Bytes(), true +} diff --git a/internal/translate/migrate/portable_test.go b/internal/translate/migrate/portable_test.go new file mode 100644 index 0000000000..612919d35b --- /dev/null +++ b/internal/translate/migrate/portable_test.go @@ -0,0 +1,46 @@ +package migrate + +import ( + "strings" + "testing" +) + +// Inlining follows what an inlined library refers to in turn: MOSA imports +// DocumentQueries, so both are appended. +func TestInlineLibrariesFollowsWhatALibraryRefersTo(t *testing.T) { + out, s := inlineLibraries([]byte("package P { private import MOSA::*; }\n")) + if want := []string{"MOSA", "DocumentQueries"}; strings.Join(s.Inlined, ",") != strings.Join(want, ",") { + t.Errorf("inlined %v, want %v", s.Inlined, want) + } + for _, decl := range []string{"library package MOSA {", "library package DocumentQueries {"} { + if !strings.Contains(string(out), decl) { + t.Errorf("output lacks %q", decl) + } + } + if len(s.NotInlined) > 0 { + t.Errorf("not inlined: %v", s.NotInlined) + } +} + +// A KerML library with no SysML spelling is left referenced and accounted for. +func TestInlineLibrariesLeavesAKerMLLibraryWithNoSysMLSpelling(t *testing.T) { + notation := []byte("part p { ref s : StateActivity::StateAction; }\n") + out, s := inlineLibraries(notation) + if string(out) != string(notation) { + t.Errorf("output changed:\n%s", out) + } + if len(s.Inlined) > 0 || s.NotInlined["StateActivity"] == "" { + t.Errorf("inlined %v, not inlined %v; want StateActivity not inlined", s.Inlined, s.NotInlined) + } + if summary := (&Report{Libraries: s}).Summary(); !strings.Contains(summary, "StateActivity not inlined: a KerML library with no SysML spelling") { + t.Errorf("summary = %s", summary) + } +} + +// A name that is a library's only as a nested member does not call for the library. +func TestInlineLibrariesIgnoresAnUnqualifiedName(t *testing.T) { + out, s := inlineLibraries([]byte("package Stochastic;\npart p;\n")) + if len(s.Inlined) > 0 || strings.Contains(string(out), "library package") { + t.Errorf("inlined %v for a package merely named like a library", s.Inlined) + } +} diff --git a/internal/translate/migrate/report.go b/internal/translate/migrate/report.go index a9a4dcc5dc..f90e5343e2 100644 --- a/internal/translate/migrate/report.go +++ b/internal/translate/migrate/report.go @@ -84,6 +84,9 @@ type Report struct { // Images counts the attached image files the migration wrote beside the // notation for its Image blocks. Images int `json:"images,omitempty"` + // Libraries accounts for the OpenSysML library packages a portable + // migration appended to the output; nil when the migration was not portable. + Libraries *LibrarySummary `json:"libraries,omitempty"` } // LayoutSummary accounts for what an MTIP export and the diagrams' own symbol @@ -198,6 +201,16 @@ func (r *Report) Summary() string { if r.Images > 0 { s += fmt.Sprintf("; wrote %d image file(s)", r.Images) } + if l := r.Libraries; l != nil { + if len(l.Inlined) == 0 { + s += "; refers to no OpenSysML library, none inlined" + } else { + s += fmt.Sprintf("; inlined %d OpenSysML library package(s): %s", len(l.Inlined), strings.Join(l.Inlined, ", ")) + } + for _, name := range sortedKeys(l.NotInlined) { + s += fmt.Sprintf("; %s not inlined: %s", name, l.NotInlined[name]) + } + } return s } diff --git a/internal/translate/migrate/results.go b/internal/translate/migrate/results.go index 38909a0bf9..beaa4460c6 100644 --- a/internal/translate/migrate/results.go +++ b/internal/translate/migrate/results.go @@ -206,7 +206,7 @@ func (m *migration) snapshotNotes(r *simresults.ConfigurationResults, scan *snap } // sortedKeys is the keys of counts, sorted. -func sortedKeys(counts map[string]int) []string { +func sortedKeys[V any](counts map[string]V) []string { keys := make([]string, 0, len(counts)) for k := range counts { keys = append(keys, k) diff --git a/packaging/man/man1/sysml.1 b/packaging/man/man1/sysml.1 index 3c0a48cc62..ea15035421 100644 --- a/packaging/man/man1/sysml.1 +++ b/packaging/man/man1/sysml.1 @@ -282,6 +282,11 @@ geometry it records as DiagramLayout annotations in the migrated views With \-migrate, the http(s) URL a comment's relative is resolved against, such as the View Editor server .TP +.B \-portable +With \-migrate: append the OpenSysML library packages the output refers to, so +the one file loads in a tool that ships only the standard library; the report +names them +.TP .BR \-compare\-results " \fIfile\fP" Run every configuration this \-migration\-results file indexes \(em or those \-action names \(em with its recorded runs and duration mode, or the \-runs diff --git a/tests/migrate/portable_test.go b/tests/migrate/portable_test.go new file mode 100644 index 0000000000..39fd6acdd1 --- /dev/null +++ b/tests/migrate/portable_test.go @@ -0,0 +1,83 @@ +package migrate_test + +import ( + "path/filepath" + "regexp" + "slices" + "strconv" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/translate/migrate" +) + +var standardDeclaration = regexp.MustCompile(`(?m)^standard library package`) + +const inlinedLibrariesMark = "\n// OpenSysML library packages the model refers to" + +// A portable migration appends every OpenSysML library package its notation +// refers to, as a library package a user file may declare, and accounts for +// them; the default migration appends none and accounts for none. +func TestPortableMigrationInlinesTheLibrariesItRefersTo(t *testing.T) { + fixtures, err := filepath.Glob(filepath.Join("testdata", "xmi", "*.xmi")) + if err != nil { + t.Fatal(err) + } + if len(fixtures) == 0 { + t.Fatal("no fixture found") + } + libs := openSysMLLibraries(t) + for _, f := range fixtures { + name := strings.TrimSuffix(filepath.Base(f), ".xmi") + t.Run(name, func(t *testing.T) { + r := migrateFixtureFileOptions(t, name, migrate.Options{Portable: true}) + model, appended, _ := strings.Cut(string(r.Notation), inlinedLibrariesMark) + if r.Report.Libraries == nil { + t.Fatal("report accounts for no libraries") + } + got := r.Report.Libraries.Inlined + var want []string + for _, lib := range libs { + if strings.Contains(model, lib+"::") { + want = append(want, lib) + } + } + for _, lib := range want { + if !slices.Contains(got, lib) { + t.Errorf("%s referred to but not accounted for in %v", lib, got) + } + if !strings.Contains(appended, "library package "+lib+" {") { + t.Errorf("%s referred to but not inlined", lib) + } + } + if len(want) == 0 && (appended != "" || len(got) > 0) { + t.Errorf("nothing referred to, yet inlined %v", got) + } + if standardDeclaration.MatchString(appended) { + t.Error("an inlined package is marked standard") + } + if summary := r.Report.Summary(); len(want) > 0 && !strings.Contains(summary, "inlined "+itoa(len(got))+" OpenSysML library package(s): ") { + t.Errorf("summary does not account for the libraries: %s", summary) + } + wantClean(t, name+".sysml", r) + }) + } + if r := migrateFixtureFile(t, "vehicle"); r.Report.Libraries != nil || strings.Contains(string(r.Notation), inlinedLibrariesMark) { + t.Error("a default migration inlined libraries") + } +} + +func itoa(n int) string { return strconv.Itoa(n) } + +// A KerML library is inlined in its SysML spelling, its functions as calc defs. +func TestPortableMigrationSpellsAKerMLLibraryInSysML(t *testing.T) { + r := migrateDocumentOptions(t, missionActivity, missionApplications, migrate.Options{Portable: true}) + wantLine(t, r.Notation, "action wait2 accept after RandomFunctions::uniform(1.0, 8.0) [SI::s];") + wantLine(t, r.Notation, "library package RandomFunctions {") + wantLine(t, r.Notation, "calc def uniform {") + wantNoLine(t, r.Notation, "function uniform {") + if got := r.Report.Libraries.Inlined; !slices.Contains(got, "RandomFunctions") { + t.Errorf("inlined %v, want RandomFunctions among them", got) + } + wantClean(t, "t.sysml", r) +} From 2e7b76e31185ffc0e7439b4acbc013c5fa023b26 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:38:01 +0000 Subject: [PATCH 7/8] fix(cli): -portable is a migration flag, refused without -migrate Co-Authored-By: jason.han --- cmd/sysml/convert_test.go | 2 + cmd/sysml/main.go | 4 + cmd/sysml/migrate.go | 1 + cmd/sysml/usage.go | 1 - packaging/man/man1/sysml.1 | 1257 ------------------------------------ 5 files changed, 7 insertions(+), 1258 deletions(-) diff --git a/cmd/sysml/convert_test.go b/cmd/sysml/convert_test.go index ed26632b55..4c2a7322d8 100644 --- a/cmd/sysml/convert_test.go +++ b/cmd/sysml/convert_test.go @@ -524,6 +524,8 @@ func TestMigrateMigratesXMI(t *testing.T) { "migrate several models": {[]string{v1, v1, "-migrate", "sysml"}, "-migrate migrates one SysML v1 model per run"}, "id with migrate": {[]string{v1, "-migrate", "ttl", "-id", "uuid"}, "-id accompanies -convert"}, "image base URL with convert": {[]string{model, "-convert", "ttl", "-image-base-url", "https://ve.example.org"}, "-image-base-url accompanies -migrate"}, + "portable with convert": {[]string{model, "-convert", "ttl", "-portable"}, "-portable accompanies -migrate"}, + "portable with validate": {[]string{model, "-validate", "-portable"}, "-portable accompanies -migrate"}, "report over the model": {[]string{xmi, "-migrate", "sysml", "-o", textReport, "-migration-report", textReport}, "-migration-report and -o both name"}, "report over the model through dangling links": {[]string{xmi, "-migrate", "sysml", "-o", danglingLink(t, dir, "model-link", "shared.txt"), "-migration-report", danglingLink(t, dir, "report-link", "shared.txt")}, "-migration-report and -o both name"}, "report over the input": {[]string{xmi, "-migrate", "sysml", "-migration-report", xmi}, "names the model being migrated"}, diff --git a/cmd/sysml/main.go b/cmd/sysml/main.go index 0739c5d355..0949e5d38e 100644 --- a/cmd/sysml/main.go +++ b/cmd/sysml/main.go @@ -545,6 +545,10 @@ func runCLI() int { fmt.Fprintln(os.Stderr, "sysml: -image-base-url accompanies -migrate of a SysML v1 model; write `sysml Model.mdzip -migrate sysml -image-base-url https://ve.example.org`") return 2 } + if portableMode && migrateFormat == "" { + fmt.Fprintln(os.Stderr, "sysml: -portable accompanies -migrate of a SysML v1 model; write `sysml Model.mdzip -migrate sysml -portable -o Model.sysml`") + return 2 + } if message := importMisuse(); message != "" { fmt.Fprintf(os.Stderr, "sysml: %s\n", message) return 2 diff --git a/cmd/sysml/migrate.go b/cmd/sysml/migrate.go index 29af7ee872..dab269107a 100644 --- a/cmd/sysml/migrate.go +++ b/cmd/sysml/migrate.go @@ -27,6 +27,7 @@ func init() { fs.StringVar(&migrationResults, "migration-results", "", "With -migrate, write the run configurations and the result snapshots the simulation tool stored for them to this JSON file, for -compare-results to read against the migrated model") fs.StringVar(&layoutPath, "layout", "", "With -migrate, read this MTIP export (HUDS XML) and write the diagram geometry it records as DiagramLayout annotations in the migrated views") fs.StringVar(&imageBaseURL, "image-base-url", "", "With -migrate, the http(s) URL a comment's relative is resolved against, such as the View Editor server") + fs.BoolVar(&portableMode, "portable", false, "With -migrate, append the OpenSysML library packages the output refers to, so the one file loads in a tool that ships only the standard library; the report names them") }) } diff --git a/cmd/sysml/usage.go b/cmd/sysml/usage.go index d93c38759a..c5ce4c4363 100644 --- a/cmd/sysml/usage.go +++ b/cmd/sysml/usage.go @@ -649,7 +649,6 @@ func registerFlags(fs *flag.FlagSet) { fs.Var(&disabledLints, "disable-lint", "Leave this lint out of the model's diagnostics: undeclared-signal, port-type-mismatch, deferred-keeper-unmarked or rounded-real-literal, comma-separated or repeated") fs.Var(&enabledLints, "enable-lint", "Report this opt-in lint, off by default, in the model's diagnostics: rounded-real-literal, comma-separated or repeated; -disable-lint wins") fs.BoolVar(&noRecordCache, "no-record-cache", false, "Parse every file loaded and hold it loaded, reading no interface record from the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0") - fs.BoolVar(&portableMode, "portable", false, "With -migrate: append the OpenSysML library packages the output refers to, so the one file loads in a tool that ships only the standard library; the report names them") fs.BoolVar(&strictMode, "strict", false, "Judge the model as conforming SysML v2: notation no pinned production admits is an error, not a warning; a SysML v1 migration writes none of it and refers to no OpenSysML library") fs.Var(&modelChecks.constraints, "constraint", "Evaluate this constraint and exit (repeatable)") fs.BoolVar(&modelChecks.selfCheck, "self-check", false, "Apply the SysMLValidation constraints to every reflectively classified model element and exit") diff --git a/packaging/man/man1/sysml.1 b/packaging/man/man1/sysml.1 index ea15035421..e69de29bb2 100644 --- a/packaging/man/man1/sysml.1 +++ b/packaging/man/man1/sysml.1 @@ -1,1257 +0,0 @@ -.\" Generated by "sysml -man"; edit the description in the command's usage.go. -.TH SYSML 1 "2026-08-30" "OpenSysML" "OpenSysML Manual" -.SH NAME -sysml \- run, check, convert and render SysML v2 and KerML models, and migrate SysML v1 -.SH SYNOPSIS -.B sysml -[options] [file...] -.SH DESCRIPTION -sysml loads the models it is given \(em a file, a directory to walk or a glob -\(em as a single model, so a declaration in one file resolves against the -others whichever order they were named in. With no expression or check to -carry out it opens an interactive prompt; otherwise it does what was asked and -exits on the verdict, which is what lets a run gate a build. -.SH OPTIONS -.SS General -.TP -.BR \-h ", " \-help -Show this help and exit -.TP -.BR \-v ", " \-version -Show the version and exit -.TP -.B \-man -Write this command's manual page, in roff, to stdout and exit -.SS Evaluating -.TP -.BR \-e ", " \-eval " \fIexpr\fP" -Evaluate this expression, against the model when one is loaded, and exit -(repeatable) -.TP -.BR \-query " \fItext\fP" -Evaluate this OSLC Query text against the model and exit -.SS Checking a model -.TP -.BR \-validate "[=\fIobject\fP]" -Report the model's diagnostics and exit, nonzero on an error; -\-validate= checks instead every assertion about that object -(repeatable) -.TP -.B \-strict -Judge the model as conforming SysML v2: notation no pinned production admits -is an error, not a warning; a SysML v1 migration writes none of it and refers -to no OpenSysML library -.TP -.BR \-disable\-lint " \fIcode\fP" -Leave this lint out of the model's diagnostics: undeclared\-signal, -port\-type\-mismatch, deferred\-keeper\-unmarked or rounded\-real\-literal, -comma\-separated or repeated -.TP -.BR \-enable\-lint " \fIcode\fP" -Report this opt\-in lint, off by default, in the model's diagnostics: -rounded\-real\-literal, comma\-separated or repeated; \-disable\-lint wins -.TP -.B \-no\-record\-cache -Parse every file loaded and hold it loaded, reading no interface record from -the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0 -.TP -.BR \-constraint " \fIname\fP" -Evaluate this constraint and exit (repeatable) -.TP -.B \-self\-check -Apply the SysMLValidation constraints to every reflectively classified model -element and exit -.TP -.BR \-self\-check\-package " \fIpackage\fP" -Apply this package's constraint defs with the SysMLValidation ones, as -\-self\-check\-package Acme::ModelingRules; implies \-self\-check (repeatable) -.TP -.BR \-requirement " \fIname\fP" -Evaluate this requirement, and every verification case verifying it, and exit -(repeatable) -.TP -.BR \-satisfy "[=\fIname\fP]" -Evaluate every satisfaction assertion, or with \-satisfy= those the -named element states, and exit (repeatable) -.TP -.BR \-calc " \fIcall\fP" -Invoke this calculation and report its result, as \-calc "Fall(3, 4)" -(repeatable) -.TP -.BR \-analysis " \fIcall\fP" -Run this analysis or verification case and report its outputs and verdict, as -\-analysis "Pkg::Case(3.0) Pkg::part" (repeatable) -.TP -.BR \-tool\-dry\-run " \fIcall\fP" -Show what the external tool the case's or action's ToolExecution names would -be given \(em manifest, executable, argv, environment, standard input and -reply mapping \(em without starting it, and discards what the run did; -repeatable -.TP -.BR \-record\-run " \fIcall\fP" -Run this analysis case as \-analysis does and record the run into the model as -AnalysisRecords elements, one record per \-sweep value or \-runs run -(repeatable) -.TP -.BR \-record\-into " \fIpackage\fP" -Record \-record\-run runs into this package instead of a Records package -beside the case's -.TP -.BR \-run\-query " \fIquery\fP" -Execute this document query and report its rows, as \-run\-query "Heavy -root=telescope" (repeatable) -.TP -.BR \-instantiate " \fIname\fP" -Create an object of this definition or usage before the checks, so a verdict -is about it (repeatable) -.TP -.B \-json -Report checks as one JSON document rather than as lines -.SS Running behaviors -.TP -.BR \-action " \fIname\fP" -Run this action to completion, as \-action "Drive rover1" to run it on an -object (repeatable) -.TP -.BR \-state " \fIname\fP" -Run this state machine, as \-state "Mission rover1" to run it on an object -(repeatable) -.TP -.BR \-advance " \fItime\fP" -Simulated time units to run the \-action and \-state behaviors for, on one -shared clock; without it a state machine takes only its initial transition -under every schedule and engine -.TP -.BR \-schedule " \fIpolicy\fP" -Policy every run resolves its choice points under: declared, reverse -(default), seed:, explore[:runs=N,depth=D] or replay: -.TP -.BR \-seed " \fIn\fP" -Seed the model's own draws \(em weighted decisions, random functions \(em and -those of \-samples and \-runs; the same seed draws the same run or table -.TP -.BR \-draws " \fIpolicy\fP" -How every run resolves the draws of RandomFunctions: random (default) draws -from \-seed; min, max and average take each call's least, greatest or mean -value and need no seed; weighted decisions draw from \-seed whatever the -policy -.TP -.BR \-clock\-step " \fIseconds\fP" -The step, in seconds, the clock of every run ticks by: a wait comes due at the -first multiple of it not before the wait ends; 0 (default) is a continuous -clock. With \-compare\-results, replaces every configuration's stepSize -.TP -.BR \-runs " \fIn\fP" -Run the \-action, or the Simulation::MonteCarlo \-analysis, this many times, -each run seeded from \-seed, and table the \-observe features or the case's -observed value; needs \-seed unless \-draws is min, max or average -.TP -.BR \-observe " \fIfeature\fP" -Report this feature of the \-runs action, or clock for the time it completed -at; default every feature it holds and the clock. With \-compare\-results, the -stored observable to compare, or = to read it from -another feature of the run (repeatable) -.TP -.BR \-sweep " \fIrange\fP" -Run the \-analysis or \-calc once per value of this range, as \-sweep -"n=1..8:2"; several ranges run their cartesian product (repeatable) -.TP -.BR \-samples " \fIn\fP" -Draw this many values uniformly from each \-sweep range instead of stepping -through it; needs \-seed -.TP -.BR \-jobs " \fIn\fP" -Runs of one check that may go concurrently, each on a worker of its own, and -files of one load that are parsed and validated at once; the result is the -same at any count. Default OPENSYSML_JOBS, else one per CPU, fewer where the -memory available leaves less than 512 MiB per worker -.SS Analysis engines -.TP -.B \-engines -List the analysis engines this build knows and their status, spawning nothing, -and exit -.TP -.B \-probe -With \-engines, also start each external engine once and report the outcome as -its status -.TP -.BR \-engine " \fIname\fP" -Analysis engine every check is put to: auto (default), all, or one by name -\(em run, explore, check, smt, sweep, solve, tool: or an external engine -.SS Checking every schedule (\-engine check, smt or all) -.TP -.BR \-check\-property " \fIname\fP" -Evaluate this constraint or requirement at every stable state of the action -and report a schedule at which it is false (repeatable) -.TP -.BR \-check\-diverge " \fIfeature\fP" -Report this feature sensitive when schedules leave it with different final -values, as x, step.out or this.level; default every attribute (repeatable) -.TP -.BR \-check\-input " \fIfeature\fP" -Under smt, leave this feature of the action free in its declared domain -although the model binds it (repeatable) -.TP -.BR \-check\-assume " \fIname\fP" -Under smt, assume this constraint or requirement over the initial state of the -action (repeatable) -.TP -.BR \-check\-witness " \fIdir\fP" -Write a witness file for each violation and each divergent value to this -directory, for \-schedule replay: and %replay to follow -.TP -.BR \-check\-depth " \fIn\fP" -The most moves one schedule may make before the search backtracks, or the -moves smt unrolls the action to (default 10000 for check, 40 for smt) -.TP -.BR \-check\-states " \fIn\fP" -Under check, the most distinct states the search may visit (default 1000000) -.TP -.BR \-check\-unroll " \fIn\fP" -Under smt, the most iterations of one loop the solver unrolls before it stops -(default 4) -.TP -.BR \-check\-timeout " \fIduration\fP" -The time the check's plan may run for, as 30s or 2m, and the time each smt -solver query may take in place of OPENSYSML_SMT_TIMEOUT -.SS Converting and migrating -.TP -.BR \-convert " \fIformat\fP" -Convert the model to this format instead of running it: sysml, kerml, ttl, -turtle, rdf or api\-json (RDF and the API element form are experimental). The -input may be a Flexo branch URL (host[:port][/base]/projects/{p}/branches/{b} -of the FLEXO_SYSMLV2_URL endpoint, or flexo://{p}/{b}), read as its RDF graph. -A SysML v1 model is refused: it is migrated, with \-migrate -.TP -.BR \-migrate " \fIformat\fP" -Migrate the SysML v1 model (.xmi, .uml or .mdzip; \-from names the format when -the extension does not) to SysML v2, written in this format: sysml, kerml, -ttl, turtle, rdf or api\-json. Ledgered, not lossless: every element is -mapped, approximated or left unmapped, and \-migration\-report says which -(experimental) -.TP -.BR \-from " \fIformat\fP" -Input format for \-convert: sysml, kerml, ttl, turtle, rdf, api\-json, or fmu -for a Functional Mock\-up Unit to import; for \-migrate: xmi, uml or mdzip, -the forms of a SysML v1 model; default the input's extension -.TP -.BR \-import " \fIfile\fP" -Set the feature values a CSV, TSV, JSON or JSON Lines file assigns, one row -per element, before \-convert writes the model; an element column names the -element and each other column a feature, as mass [kg] (repeatable) -.TP -.BR \-import\-as " \fIshape\fP" -What \-import makes of each row: values, which sets the features of the -element the row names. The default is values. -.TP -.BR \-import\-map " \fIfile\fP" -Read the \-import before it onto elements and features as this JSON mapping -file says -.TP -.BR \-import\-format " \fIformat\fP" -Read the \-import before it as csv, tsv, json or jsonl rather than as its -extension says -.TP -.B \-import\-dry\-run -Report the values \-import would set, changing and writing nothing -.TP -.BR \-id " \fIname\fP" -With \-convert ttl or api\-json, how derived element ids are spelled: -qualified (default) derives each from its qualified name; uuid mints -name\-based uuids under each root package, as the library convention does -.TP -.BR \-o ", " \-output " \fIfile\fP" -Write what \-convert, \-migrate, \-compile, \-render or \-render\-document -produces to this file instead of stdout; with \-convert ttl or \-migrate ttl, -a Flexo branch URL pushes the graph to the branch -.TP -.BR \-migration\-report " \fIfile\fP" -With \-migrate, write the element\-by\-element migration report to this file: -JSON when it ends in .json, text otherwise -.TP -.BR \-migration\-results " \fIfile\fP" -With \-migrate, write the run configurations and the result snapshots the -simulation tool stored for them to this JSON file, for \-compare\-results to -read against the migrated model -.TP -.BR \-layout " \fIfile\fP" -With \-migrate, read this MTIP export (HUDS XML) and write the diagram -geometry it records as DiagramLayout annotations in the migrated views -.TP -.BR \-image\-base\-url " \fIurl\fP" -With \-migrate, the http(s) URL a comment's relative is resolved -against, such as the View Editor server -.TP -.B \-portable -With \-migrate: append the OpenSysML library packages the output refers to, so -the one file loads in a tool that ships only the standard library; the report -names them -.TP -.BR \-compare\-results " \fIfile\fP" -Run every configuration this \-migration\-results file indexes \(em or those -\-action names \(em with its recorded runs and duration mode, or the \-runs -and \-draws given, seeded from \-seed, and table the tool's and OpenSysML's -min, mean, p50, p90 and max of each observable with their relative difference -.SS Compiling natively -.TP -.BR \-compile " \fIcalc\fP" -Compile this calc def to a native executable named by \-o, as \-compile -Pkg::Fib -.TP -.BR \-target " \fIbackend\fP" -Backend \-compile generates code for: c or go. The default is c. -.TP -.B \-source -With \-compile, write the generated source to \-o instead of building it -.SS Rendering views -.TP -.BR \-render " \fIview\fP" -Render this view of the model instead of running it, in the form its render -member states; # renders every file loaded and #: one -element, kind being tree, interconnection, state, action, case, mixed, -sequence, table or matrix, without a declared view -.TP -.BR \-render\-all " \fIdir\fP" -Render every declared view into this directory -.TP -.BR \-graphs " \fIname\fP" -Write the lowered graph of this action or state machine, and of every behavior -it performs, as the canonical graphs:1 JSON an external analysis engine is -sent, to stdout or \-output -.TP -.BR \-render\-form " \fIform\fP" -Form \-render or \-render\-all writes: text, mermaid, markdown, dot, plantuml, -d2, csv or tsv (csv and tsv for a table or matrix); D2 writes tree, -interconnection, state, action, sequence, requirement, definition and package -renderings, not case or mixed; default from the destination for \-render, each -kind's machine form for \-render\-all -.TP -.BR \-render\-palette " \fIpalette\fP" -Palette the dot, mermaid, plantuml or d2 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 -.TP -.BR \-render\-link " \fItemplate\fP" -Source link template for \-render, \-render\-all, \-render\-document, -\-render\-documents and \-render\-run. Placeholders: {file}, {line}, {col}, -{qname}, {id}; {file} is the path as loaded (use absolute paths for vscode:// -or file:// links) -.TP -.BR \-render\-unplaced " \fIplacement\fP" -Where a graph form of a view some Layout positions puts the nodes none does: -omit (default) leaves them undrawn in every form, strip draws them, in rows -below the dot drawing; applies to \-render, \-render\-all and document -diagrams -.TP -.BR \-render\-style " \fIstyle\fP" -Drawing style of the dot or mermaid form: pilot (default), the Pilot -visualizer's black and white, or cameo, the look of Cameo Systems Modeler; -applies to \-render, \-render\-all and document diagrams -.TP -.BR \-render\-ports " \fIdisplay\fP" -How much of a part's ports \-render or \-render\-all draws on an -interconnection or mixed rendering: minimal (default), the ports its -interconnection edges end at, each a small square on the part's border named -beside it, or full, every port, labelled name : Type -.TP -.BR \-render\-overlay " \fIoverlay\fP" -What \-render or \-render\-all draws over a requirement rendering's structure: -verdicts runs the verification cases verifying each requirement and colours -and labels it by their verdicts; default none, a purely structural drawing -.SS Listing documents and views -.TP -.BR \-list " \fIwhat\fP" -List the model's documents, views, diagrams (the graph\-shaped views), -pseudo\-views or all of them, by the qualified names \-render\-document and -\-render read: documents, views, diagrams, pseudo\-views or all -.TP -.BR \-list\-kind " \fIkinds\fP" -Comma\-separated view kinds \-list views, diagrams or all keeps, as -state,action -.TP -.BR \-list\-form " \fIform\fP" -Form \-list writes: text (default), one aligned line per item, tsv or json -.SS Rendering documents -.TP -.BR \-render\-document " \fIname\fP" -Compile this document definition, run its queries and write the rendered -document -.TP -.BR \-render\-documents " \fIdir\fP" -Render every document definition, linked to one another, into this directory; -a document that cannot be rendered gets a page stating why and the run exits 3 -.TP -.BR \-doc\-form " \fIform\fP" -Form the documents are written in: markdown (default), html or pdf, which -drives an external converter -.TP -.BR \-diagram\-form " \fIform\fP" -Form the documents' graph\-shaped diagrams are written in: mermaid, dot, -plantuml or d2; D2 writes tree, interconnection, state, action, sequence, -requirement, definition and package renderings, not case or mixed; unset, a -positioned view is dot and any other mermaid; a table or matrix view is a -table either way -.TP -.B \-doc\-title\-page -Put the document title on a page of its own (html or pdf) -.TP -.B \-doc\-toc -Write a table of contents ahead of the content (html or pdf) -.TP -.B \-doc\-number\-sections -Number the section headings hierarchically (html or pdf) -.TP -.B \-doc\-number\-figures -Number the figures and tables in their captions, Figure 1. and Table 1. in -document order (markdown, html or pdf) -.TP -.BR \-pdf\-engine " \fIconverter\fP" -Converter \-doc\-form pdf drives: weasyprint (default), pandoc or prince -.SS Styling HTML documents -.TP -.BR \-html\-theme " \fItheme\fP" -Style the HTML page or PDF with a bundled theme layered over the default -stylesheet: default, acm, ieee, modern, nasa, print or report -.TP -.BR \-html\-css " \fIfile|url\fP" -Style the HTML or PDF with this stylesheet too: a file is inlined, a URL is -linked (repeatable, applied in order after the default sheet) -.TP -.B \-html\-no\-default\-css -Leave the default stylesheet out, so only \-html\-css sheets style the HTML or -PDF -.TP -.B \-html\-fragment -Write the document element alone, without the page shell or a stylesheet, to -embed in a page of your own -.TP -.B \-html\-default\-css -Write the default document stylesheet, or with \-html\-theme that theme's -whole sheet, and exit -.TP -.BR \-html\-mermaid " cdn|\fIurl\fP" -Have the page load Mermaid to draw its diagrams: cdn loads a pinned release -from jsDelivr, a URL the script it names -.TP -.BR \-html\-math " cdn|\fIurl\fP" -Have the page load MathJax to typeset its formulas: cdn loads a pinned release -from jsDelivr, a URL the script it names -.SS Syncing against a repository -.TP -.BR \-sync\-diff " \fIrepo\fP" -Show the change set between the model and this repository \(em a graph file -(.ttl) or a SysML v2 API endpoint URL \(em and exit; never writes -.TP -.BR \-sync\-apply " \fIurl\fP" -Apply the change set to the model's project branch at this SysML v2 API -endpoint URL, then record the commit in the sync state -.TP -.BR \-sync\-base " \fIfile\fP" -Repository graph at the last\-seen commit; with it, repository changes since -then surface as conflicts -.TP -.BR \-sync\-state " \fIfile\fP" -Sync state file recording project, branch and last\-seen commit; default -.sync.json beside the model -.TP -.B \-sync\-confirm\-deletes -Confirm repository\-side deletes; without it the diff reports them but -applying is refused -.TP -.B \-sync\-mint\-ids -Mint a UUID for each unannotated element being created, so the repository can -address it stably -.TP -.BR \-sync\-annotate " \fIfile\fP" -Write the model to this file with each minted id declared as an @ElementId -annotation (needs \-sync\-mint\-ids) -.SS Diagnostics and profiling -.TP -.B \-debug -Report every diagnostic over the whole session buffer, with the pass that -produced it -.TP -.B \-quiet -Report errors only, suppressing warnings -.TP -.B \-trace -Report each execution step: expression evaluation, calc invocation, action -tokens, state transitions -.TP -.BR \-render\-run " \fIkind\fP=\fIpath\fP" -Render the trace of a run as timeline or sequence into path (\- is stdout); -form comes from \-render\-form or the path extension -.TP -.BR \-cpuprofile " \fIfile\fP" -Write a CPU profile of the run to this file, for go tool pprof -.TP -.BR \-memprofile " \fIfile\fP" -Write a heap profile of the run to this file, for go tool pprof -.TP -.B \-memstats -Report on stderr what the run cost: wall time, memory allocated, memory taken -from the OS -.SS Deprecated -.TP -.BR \-to " \fIformat\fP" -Replaced by \-convert, which names the output format -.TP -.B \-pdf\-title\-page -Former name of \-doc\-title\-page, which also shapes HTML -.TP -.B \-pdf\-toc -Former name of \-doc\-toc, which also shapes HTML -.TP -.B \-pdf\-number\-sections -Former name of \-doc\-number\-sections, which also shapes HTML -.SH EXAMPLES -.RS 2 -.nf -sysml # Start interactive REPL -sysml \-e "5 + 3" # Evaluate and exit -sysml \-e "expr" file.sysml # Load file, evaluate, and exit -sysml file.sysml # Load file and start REPL -sysml \-debug file.sysml # Load file, reporting every diagnostic -sysml \-trace file.sysml # Load file, reporting each execution step -.fi -.RE -.SH CHECKING A MODEL -.RS 2 -.nf -# Evaluate one constraint and exit -sysml \-constraint MassBudget model.sysml -# Evaluate one requirement and exit -sysml \-requirement PowerMargin model.sysml -# Evaluate every satisfaction assertion -sysml \-satisfy model.sysml -# \&...only the ones Ctx states -sysml \-satisfy=Ctx model.sysml -# Check C against an object of p -sysml \-instantiate p \-constraint C model.sysml -# Report diagnostics only -sysml \-validate model.sysml -# \&...asking whether it is conforming SysML v2 -sysml \-validate \-strict model.sysml -# Apply the SysMLValidation constraints -sysml \-self\-check model.sysml -# \&...plus a rule package's constraints -sysml \-self\-check\-package Acme::Rules m.sysml -# Check every assertion about an object -sysml \-instantiate car \-validate=car m.sysml -# Invoke a calculation -sysml \-calc "Fall(3, 4)" model.sysml -# Run an analysis case -sysml \-analysis shipCost model.sysml -# \&...on an object as its subject -sysml \-analysis "CostAnalysis ship" model.sysml -# Run a verification case for its verdict -sysml \-analysis speedCheck model.sysml -# Execute a document query -sysml \-run\-query "Heavy root=scope" model.sysml -# Run an action to completion -sysml \-action Drive model.sysml -# Run a state machine for 10 time units -sysml \-state Mission \-advance 10 model.sysml -# \&...an action and a machine on one clock -sysml \-action Ping \-state Sm \-advance 5 m.sysml -# Run every linearization; table the outcomes -sysml \-schedule explore \-action Drive m.sysml -# Report the verdicts as JSON -sysml \-satisfy \-json model.sysml -.fi -.RE -.PP -Check flags may be combined. \-validate= checks every assertion about -an object \-instantiate created and the objects it holds: an object named, -#, or a path such as car.engine. \-self\-check applies the OpenSysML -SysMLValidation constraints to each reflectively classified element; underived -reflective features are reported as unevaluated, not as violations. -\-self\-check\-package adds the constraint defs of a package the loaded files -declare to the same walk, as \-self\-check\-package Acme::ModelingRules: a -rule is a constraint def whose first in parameter is typed by a SysML or KerML -metaclass, and it applies to every element that metaclass conforms to. -\-requirement and \-satisfy report beside each verdict the verdict of every -verification case verifying the requirement. \-analysis takes arguments for -the case's inputs and an object as its subject, as \-analysis "Pkg::Case(3.0) -Pkg::part"; a verification case also reports the verdict its body produced. -.SH RUNNING ACTIONS AND STATE MACHINES -\-action runs an action to completion and \-state a state machine, both on one -shared clock that \-advance runs for the given simulated time units; without -\-advance a state machine takes only its initial transition. Either names a -definition or usage, or one followed by the object to perform it on, as -\-action "Drive rover1". -.PP -\-schedule is the policy every run resolves its choice points under \(em -concurrent tokens, overlapping guards, competing transitions: declared, -reverse (the default), seed: for a reproducible pseudo\-random order, -explore[:runs=N,depth=D] to run every linearization within the budget and -table the distinct outcomes, or replay: to follow a witness file's -choice lines move for move, then reverse. -.PP -Under \-schedule explore, \-engine check, smt or all, each run creates objects -of its own before the behaviors start: one per \-instantiate, which a \-state -or \-action named alone attaches to, and one per \-action or \-state naming a -definition or usage to create or a path into one such as mission.rover. The -declaration is created once per run, so machines on sibling parts share it and -its connectors. -.PP -\-jobs is how many runs of one check may go concurrently \(em the -linearizations of an exploration, the engines \-engine all consults \(em each -on a worker of its own over the shared model, and how many files of one load -are parsed and validated at once; the result is the same at any count. By -default one per CPU, fewer where the memory available leaves less than 512 MiB -per worker. -.SH ANALYSIS ENGINES -.RS 2 -.nf -# List engines, kind and status; nothing runs -sysml \-engines -# Also start each external engine once -sysml \-engines \-probe -# The same as \-schedule explore -sysml \-engine explore \-action Drive m.sysml -# Every engine that covers the question -sysml \-engine all \-requirement R m.sysml -# One engine; the plan in the report -sysml \-engine run \-json \-constraint C m.sysml -.fi -.RE -.PP -Every check is a question put to an analysis engine: run executes the model -once under \-schedule, explore runs every linearization, sweep runs the rows -of \-sweep, and solve puts condition sets to an SMT solver. \-engine auto (the -default) picks the engine of highest authority that covers the question and -falls back to the next when it refuses or covers nothing; \-engine puts -the question to that engine alone, and its refusal is the answer; \-engine all -puts it to every engine that covers it, one after another in name order, and -composes their answers: a witnessed violation stands over any universal claim, -and a contradiction is reported as a disagreement decided in the interpreter's -favor. -.PP -Every verdict is followed by its standing: the claim, the strength of the -evidence \(em observed (one run), witnessed (a replayed execution), bounded -(every case within a budget), proved (every case) \(em and what earned it, as -`holds (observed: 1 run under reverse)`. A budget the engine reached lowers -the strength and is named. Under \-json each check carries the plan and one -results[] entry per engine that answered, with its engine, claim, strength, -bounds and witness. -.PP -\-engines lists each engine's name, kind (built\-in, tool, engine, policy, -sampler), protocol, authority, the questions it answers and whether its -process is found, with the manifest entry and resolved command of each -external one; \-probe also starts each external engine once and checks its -describe against its manifest entry field by field. An engine is named run, -explore, check, smt, sweep, solve, tool: from OPENSYSML_TOOLS, or as an -external engine from OPENSYSML_ENGINES. \-engine explore is \-schedule -explore; \-engine check searches every schedule of each \-action for a -violation, deadlock, failure or divergence; \-engine smt decides each -\-check\-property over every schedule and every value of the action's free -inputs through an SMT solver. -.SH CHECKING EVERY SCHEDULE -.RS 2 -.nf -# Search every schedule for Safe -sysml \-engine check \-action Go \-check\-property Safe m.sysml -# Is speed schedule\-dependent? -sysml \-engine check \-action Go \-check\-diverge speed m.sysml -sysml \-engine smt \-action Go \-check\-input t \-check\-property Safe \e - m.sysml -# A witness file per finding -sysml \-engine check \-action Go \-check\-witness out m.sysml -.fi -.RE -.PP -\-check\-property evaluates a constraint or requirement at every stable state -of the action, on the performing object when there is one, and reports a -schedule at which it is false. \-check\-diverge reports a feature sensitive -when schedules leave it with different final values \(em check searching the -schedules, smt asking the solver for two that end it apart \(em named as x, -step.out for a performed node's output or this.level for the performing -object's (not covered under smt); a name nothing holds is refused, and without -it every attribute of the action and of its performing object is checked, the -action's own when it has none. -.PP -Under smt every input the model leaves unbound is free and every bound one is -pinned; \-check\-input leaves a bound feature free in its declared domain, and -a name that is not a feature the action reads is refused. \-check\-assume -assumes a constraint or requirement over the initial state; a set no initial -state satisfies is reported not covered, never proved. -.PP -\-check\-witness writes a file for each violation and each divergent value -\(em the inputs the solver chose, the schedule's choices, a blank line, then -the run's trace \(em which \-schedule replay: and %replay follow. A -bound is named when it is hit: \-check\-depth is the most moves one schedule -may make before the search backtracks, or the moves smt unrolls the action to; -\-check\-states the most distinct states the search may visit, the same figure -\-engine all gives an exploration as its runs; \-check\-unroll the most -iterations of one loop smt unrolls; \-check\-timeout the time the plan may run -for and the time each solver query may take in place of OPENSYSML_SMT_TIMEOUT -\(em a search it stops is reported incomplete with the states and depth -reached, not as a verdict. -.SH SWEEPING AND SAMPLING A PARAMETER -.RS 2 -.nf -# One run per range value -sysml \-calc Twice \-sweep "n=1..8:2" m.sysml -# Every combination of the two -sysml \-analysis C \-sweep "m=1..3" \-sweep "v=1..2" m.sysml -# 20 values drawn uniformly -sysml \-analysis C \-sweep "m=1..9" \-samples 20 \-seed 7 m.sysml -# Report the rows as JSON -sysml \-analysis C \-sweep "m=1..3" \-json m.sysml -.fi -.RE -.PP -A sweep is orchestration over the ordinary run: each row is one \-analysis or -\-calc invocation with the swept parameter bound to that row's value and every -other argument as given, so nothing about how a case executes changes. Name a -single \-analysis or \-calc; a parameter the case does not declare, one the -invocation already binds, a step of zero or one pointing away from the end of -the range is an error rather than a silent row. -.PP -An endpoint and a step carry the syntax and the units an argument carries -("mass=0.0 [SI::kg]..9.0 [SI::kg]:3.0 [SI::kg]"), and the end of the range is -included where the step lands on it. -.PP -The values are produced in the swept parameter's declared type, not the -literals': a Real parameter swept over 1..4:1 is bound to 1.0, 2.0, 3.0, 4.0 -and an Integer one swept over 1.0..3.0:1.0 to 1, 2, 3, while an endpoint or -step no Integer parameter can take (:0.5) is refused before any run, as is a -range over a Boolean, String, enumeration or non\-scalar parameter. A -parameter declaring no type takes the range as written, and the table says so. -.PP -A range between whole numbers steps by one where no : is written; one -with a fractional endpoint needs one. A range read as reals takes an Integer -endpoint or step only where a Real holds it without rounding, and steps only -where the reals tell its rows apart. Rows come out in the order the ranges -were given, the first varying slowest, and a run that failed is a row carrying -its error rather than the end of the table. -.PP -\-samples draws that many values uniformly from each range instead of stepping -through it \(em Integers inclusively for a parameter taking Integers, reals in -[, ) for one taking reals \(em and needs \-seed: the same seed draws -the same table on every platform. Uniform is the only distribution on offer -\(em the bundled library states no probability distributions \(em so a range -must be written .. rather than as a named distribution. -.SH RUNNING AN ACTION MANY TIMES -.RS 2 -.nf -# 100 runs, every feature -sysml \-action Acquire \-runs 100 \-seed 7 m.sysml -# One observable -sysml \-action Acquire \-runs 100 \-seed 7 \-observe elapsed m.sysml -# One run, its draws seeded -sysml \-action Acquire \-seed 7 m.sysml -# Durations at their max -sysml \-action Acquire \-draws max m.sysml -# Mean durations -sysml \-action A \-runs 9 \-seed 7 \-draws average m.sysml -# A MonteCarlo analysis case -sysml \-analysis "Mc obj" \-runs 100 \-seed 7 m.sysml -.fi -.RE -.PP -\-runs runs one \-action to completion that many times, each run on a fresh -context whose modeled randomness \(em the weighted decisions @Probability -states and the draws of uniform, uniformInteger, triangular and normal \(em is -seeded from a seed of its own derived from \-seed, so the same seed makes the -same table on every platform and a run can be replayed alone. \-schedule stays -the second, independent knob: it decides the concurrency choices, which carry -no probability, in every run alike. -.PP -\-draws is the third knob, the duration policy: random (the default) draws -every RandomFunctions call from the seed; min, max and average resolve each -call to the least, greatest or mean value of its distribution instead \(em the -midpoint of a uniform, the mean of a triangular \(em so a run whose only -randomness is durations completes deterministically and needs no \-seed. -normal has no least or greatest value, so a run that calls it under min or max -stops with an error. Weighted decisions are not durations: they draw from the -seed under every policy, and an unseeded run takes the most probable branch. -The policy is written into every witness as `draws by `, and -\-schedule replay: follows it. -.PP -The table has one row per run, numbered, with each \-observe feature of the -action \(em `clock` is the simulation time the run completed at, never a -feature of that name \(em and without \-observe every feature the action holds -and the clock. Below it each numeric observable is summarised over the -completed runs: min, mean, max, the nearest\-rank p50 and p90, and a -histogram; a non\-numeric one is counted by value. A feature the action does -not hold is refused. -.PP -\-runs also runs an \-analysis that specializes Simulation::MonteCarlo, the -OpenSysML library's analysis of repeated runs: each run performs the case's -steps on a fresh subject, seeded as an action's run is, and reads the value -the case binds as observed; the table has one row per run with that value, and -beneath its distribution the case is concluded once over the sample \(em runs, -mean, deviation (the sample standard deviation) and outOfSpec (the runs in -which a check of the case did not hold) bound, and the case's own outputs and -checks evaluated over them. Each run makes its objects from their -declarations, so a subject named by `#id` alone is refused. Run once, without -\-runs, such a case leaves its statistics unbound. \-observe belongs to an -action's runs, not a case's. -.SH CONVERSION -.RS 2 -.nf -# SysML notation to RDF Turtle, on stdout -sysml model.sysml \-convert ttl -# RDF Turtle to SysML notation -sysml model.ttl \-convert sysml -# Write the conversion to a file -sysml model.sysml \-convert ttl \-o m.ttl -# Name the input format explicitly -sysml in.txt \-convert ttl \-from sysml -# A Flexo branch as notation -sysml flexo://demo/main \-convert sysml -# Push the graph to the branch -sysml model.sysml \-convert ttl \-o flexo://demo/main -.fi -.RE -.PP -The input format is taken from the file extension (.sysml, .kerml, .ttl, -\&.json, .fmu) unless \-from names it: sysml, kerml, ttl, turtle, rdf, -api\-json, or fmu for a Functional Mock\-up Unit to import. Converting to the -format it is already in rewrites the input: notation is reformatted, Turtle is -normalized. -.PP -A SysML v1 model (.xmi, .uml, .mdzip) is refused: it is migrated, not -converted; see Migration. -.PP -Either side may name a Flexo MMS project branch instead of a file: -http(s)://host[:port][/base]/projects/{project}/branches/{branch}, or -flexo://{project}/{branch}, both naming the endpoint FLEXO_SYSMLV2_URL -configures. A branch input is read as its head commit's RDF graph; a branch -\-o takes the \-convert ttl output as the branch's whole model graph, -conditional on the branch's etag, and refuses a head the sync state says has -moved. Both need the bearer token in FLEXO_INTEROP_TOKEN and record the head -commit in the sync state (\-sync\-state, or .sync.json on a read / -.sync.json on a push). -.PP -RDF conversion \(em Turtle and the API's JSON element form alike \(em is -experimental: the mapping covers model structure and the behavior its bodies -state, refuses what it cannot write back, and its vocabulary may change -without a compatibility path; see docs/reference/rdf\-mapping.md \(sc Status -.PP -Every run that converts RDF says so on stderr. Saving to .sysml or .kerml is -stable. -.SH MIGRATION -.RS 2 -.nf -# A Cameo project as notation -sysml Model.mdzip \-migrate sysml \-o Model.sysml -sysml Model.mdzip \-migrate sysml \-o Model.sysml \-migration\-report \e - Model.report.txt -# A v1 XMI export as RDF Turtle -sysml Model.xmi \-migrate ttl \-o Model.ttl -# Views laid out from MTIP -sysml Model.mdzip \-migrate sysml \-layout Model_mtip.xml -# Index the tool's results -sysml Model.mdzip \-migrate sysml \-migration\-results runs.json -# Then compare with the tool's -sysml Model.sysml \-compare\-results runs.json -# Name the v1 form explicitly -sysml export.xml \-from xmi \-migrate sysml -.fi -.RE -.PP -A SysML v1 model \(em UML XMI 2.5.1 with the SysML profile applied, an Eclipse -UML2 .uml file, or a Cameo/MagicDraw .mdzip archive \(em is migrated to SysML -v2, not converted: the migration is ledgered, not lossless. Every v1 element -gets one of three verdicts, mapped (written as the v2 construct it corresponds -to), approximated (written as the nearest v2 construct, with a note saying -what differs) or unmapped (left behind, with the reason); profile, library and -notation content nothing refers to is skipped. The one\-line summary of the -verdicts goes to stderr; \-migration\-report writes the element\-by\-element -ledger to a file, JSON when it ends in .json, text otherwise. \-convert -refuses a v1 model for this reason. -.PP -\-from names the v1 form (xmi, uml or mdzip) when the extension does not; -\-migrate writes sysml, kerml, ttl, turtle, rdf or api\-json, and refuses an -input that is not v1, which \-convert handles. \-layout names an MTIP export -of the same project, whose diagram geometry is written into the migrated views -as DiagramLayout metadata; \-image\-base\-url resolves the relative -of a comment to the server serving it; \-migration\-results writes the -simulation tool's run configurations and the result snapshots it stored as a -JSON sidecar, which \-compare\-results on the migrated model runs against; -\-strict writes only notation a pinned SysML v2 production admits, reporting a -construct whose only v2 form is an OpenSysML extension as unmapped. The images -a document's Image blocks carry are written beside \-o, so a migration that -wrote any needs \-o a local file path. -.PP -SysML v1 migration is experimental: the mapping covers structure, ports and -connectors, requirements, constraints, instances and allocations, reports -every element it approximates or leaves behind, and what it writes for a v1 -element may change without a compatibility path; see -docs/reference/sysml\-v1\-migration.md \(sc Status -.PP -Every run that migrates a v1 model says so on stderr. -.SH NATIVE COMPILATION -.RS 2 -.nf -# Compile a calc def to a C executable -sysml model.sysml \-compile Pkg::Fib \-o fib -# \&...via Go -sysml model.sysml \-compile Pkg::Fib \-target go \-o fib -# Write the generated source only -sysml model.sysml \-compile Pkg::Fib \-source \-o fib.c -.fi -.RE -.PP -The executable takes the calc's parameters as arguments and prints its result; -it computes what sysml \-calc computes, or fails with the same error. Only the -scalar subset compiles (Integer, Real, Boolean; see -docs/project/native\-compilation.md). -.SH SYNCING AGAINST A REPOSITORY -A dry run unless \-sync\-apply: -.PP -.RS 2 -.nf -# Show the change set and exit -sysml model.sysml \-sync\-diff repo.ttl -sysml model.sysml \-sync\-diff repo.ttl \-sync\-base last\-seen.ttl -sysml model.sysml \-sync\-diff repo.ttl \-sync\-confirm\-deletes -sysml model.sysml \-sync\-diff repo.ttl \-sync\-mint\-ids \-sync\-annotate \e - out.sysml -# Against the live API; no writes -sysml model.sysml \-sync\-diff http://localhost:8083 -# Write the change set as a commit -sysml model.sysml \-sync\-apply http://localhost:8083 -.fi -.RE -.PP -The diff correlates elements by their effective id \(em an @ElementId -annotation, or the encoded qualified name \(em so a rename, move or retype is -an update, never a delete plus a create. Repository\-only elements are -reported as deletes but applying them needs \-sync\-confirm\-deletes; -conflicts \(em a declared id the branch no longer has, or a repository change -since the last\-seen commit \(em exit 1 and are never resolved silently. -.PP -The last\-seen commit is tool state in .sync.json (or \-sync\-state), -never written into the notation. \-sync\-apply refuses a change set the dry -run would have flagged, sends each update under its retained id, and records -the resulting commit; the token comes from FLEXO_INTEROP_TOKEN. -.SH RENDERING A VIEW -.RS 2 -.nf -# ASCII text at a terminal -sysml model.sysml \-render Views::vehicleView -sysml model.sysml \-render Views::vehicleView \-render\-form markdown -sysml model.sysml \-render Views::vehicleView \-o view.mmd -sysml model.sysml \-render Views::vehicleView \-render\-form dot -sysml model.sysml \-render Views::vehicleView \-render\-form mermaid \e - \-render\-palette okabe\-ito -# unpositioned nodes in a strip below -sysml model.sysml \-render Views::vehicleView \-render\-form dot \e - \-render\-unplaced strip -# drawn in Mermaid's Cameo approximation -sysml model.sysml \-render Views::vehicleView \-render\-form mermaid \e - \-render\-style cameo -# drawn as Cameo draws it -sysml model.sysml \-render Views::vehicleView \-render\-form dot \e - \-render\-style cameo -# every port of every part, typed -sysml model.sysml \-render Views::loopView \-render\-form dot \e - \-render\-ports full -sysml model.sysml \-render Views::vehicleView \-render\-form plantuml \-o \e - view.puml -sysml model.sysml \-render Views::vehicleView \-render\-form d2 \-o view.d2 -sysml model.sysml \-render Views::partsTable \-render\-form csv \-o \e - parts.csv -# several files, loaded as one model -sysml types.sysml model.sysml \-render Views::vehicleView -sysml model.sysml \-render\-all rendered -.fi -.RE -.PP -The rendering is the one the view's render member states, and a containment -tree where it states none. It is tool\-defined output: SysML v2 specifies the -notation, not how a tool draws it. Notices \(em an empty view, an element the -rendering cannot represent \(em go on stderr. Every file named is loaded as -one model, so a view may expose elements a sibling file declares. -\-render\-form names the form written \(em text, mermaid, markdown, dot, -plantuml, d2, csv or tsv; by default \-render takes it from the destination -and \-render\-all writes each kind's machine form, one file per view named by -its qualified name with :: as . and every byte unsafe in a filename (/, \e, :, -\&., %, control characters, what Windows reserves) as %XX; a name past 255 bytes -is cut and tagged ~ and a hash of the whole. A graph\-shaped rendering -defaults to Mermaid. DOT writes tree, interconnection, state, action, case, -mixed, requirement, definition and package renderings; PlantUML writes those -kinds and sequence. D2 writes tree, interconnection, state, action, sequence, -requirement, definition and package renderings; case and mixed renderings -refuse D2 with a typed error. Table and matrix renderings refuse graph forms -with a typed error. Neither Graphviz, PlantUML nor D2 is needed to write graph -forms. A table or matrix is written as a Markdown table by default, and as -comma\- or tab\-separated values with \-render\-form csv or tsv: a header -record of the columns, then a record per row, quoted as RFC 4180 quotes a -field. Graph forms are drawn in the black\-and\-white style of the SysML v2 -Pilot visualizer; \-render\-palette fills the DOT, Mermaid, PlantUML and D2 -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. \-render\-style names the -look the DOT and Mermaid forms draw in: pilot (default), the Pilot -visualizer's, or cameo, the look of Cameo Systems Modeler \(em a diagram frame -with a header tab, Arial text, gradient fills, compartments and the UML -pseudo\-state symbols \(em for a diagram migrated from Cameo to keep its look. -An interconnection or mixed rendering draws on each part the ports its -interconnection edges end at, each a small square on the part's border named -beside it; \-render\-ports full draws every port a part has, labelled name : -Type. A DiagramLayout Style on a member colours it over either look, and a -Note is drawn beside the member it is about. A view whose members carry -DiagramLayout positions draws the placed members and the edges between them in -every graph form, and leaves a member with no position undrawn: the DOT form -pins each at its stated place, so nothing lands on a positioned box, while -Mermaid, PlantUML and D2 lay the same members out themselves. -\-render\-unplaced strip draws the unplaced members too, in rows in a strip -below a DOT drawing. The same setting shapes the diagrams of -\-render\-document and \-render\-documents. -.SH LISTING DOCUMENTS AND VIEWS -.RS 2 -.nf -# the names \-render\-document reads -sysml model.sysml \-list documents -# the names \-render reads, with their kinds -sysml model.sysml \-list views -sysml model.sysml \-list diagrams \-list\-kind state,action -sysml model.sysml \-list all \-list\-form json -.fi -.RE -.PP -\-list documents lists the document definitions the loaded model declares, -\-list views its views with the rendering kind each states, a view whose kind -this build does not produce marked unsupported with the reason, \-list -diagrams the graph\-shaped views \(em tree, interconnection, state, action, -case, mixed, sequence, requirement, definition and package, the kinds a -document draws as a diagram \(em \-list pseudo\-views the pseudo\-views a -document declaring no view is rendered through, and \-list all the documents -followed by the views. Each name is the qualified name as the notation writes -it, every segment quoted that needs it and a line break escaped as \en, so it -is passed back unchanged to \-render\-document or \-render. -.PP -\-list\-kind keeps the views of the comma\-separated kinds it names. -\-list\-form text (default) writes one aligned line per item; tsv writes a -header record category, kind, supported, file, line, name and reason, then a -record per item; json writes an array of objects with those fields, each -omitted when empty. file is the path the declaration was loaded from and line -its 1\-based line. A model declaring nothing to list writes nothing. -.SH RENDERING A DOCUMENT -.RS 2 -.nf -# Markdown on stdout -sysml model.sysml \-render\-document Reports::MassReport -sysml model.sysml \-render\-document Reports::MassReport \-o report.md -sysml model.sysml \-render\-document Reports::MassReport \-doc\-form pdf \e - \-o report.pdf -sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e - \-o report.html -# every document, linked -sysml model.sysml \-render\-documents rendered -sysml model.sysml \-render\-documents site \-doc\-form html \-html\-css \e - theme.css -# a bundled theme -sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e - \-html\-theme report \-o report.html -# diagrams drawn in the browser -sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e - \-html\-mermaid cdn \-o report.html -# formulas typeset in the browser -sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e - \-html\-math cdn \-o report.html -# diagrams as Graphviz DOT -sysml model.sysml \-render\-document Reports::MassReport \-diagram\-form \e - dot \-o report.md -# diagrams as PlantUML -sysml model.sysml \-render\-document Reports::MassReport \-diagram\-form \e - plantuml \-o report.md -# diagrams as D2 -sysml model.sysml \-render\-document Reports::MassReport \-diagram\-form \e - d2 \-o report.md -sysml model.sysml \-render\-document Reports::MassReport \-doc\-form pdf \e - \-pdf\-engine pandoc \-doc\-title\-page \-doc\-toc \-doc\-number\-sections \e - \-o report.pdf -# the default stylesheet -sysml \-html\-default\-css \-o sysml\-document.css -# a theme's full sheet -sysml \-html\-default\-css \-html\-theme modern \-o modern.css -.fi -.RE -.PP -A document is a part def specializing DocumentQueries::Document. Its queries -are bound in the model and run against it, and the result is written as -CommonMark\-compatible Markdown. Its diagram blocks are chosen per diagram: a -view some DiagramLayout::Layout or Route positions is drawn by Graphviz where -it states, as inline SVG when dot is installed and as a dot fence otherwise, -and every other graph\-shaped view is Mermaid source; with Graphviz absent a -positioned view falls back to Mermaid under a notice saying so. -\-diagram\-form mermaid, dot, plantuml or d2 writes every graph\-shaped one in -that form instead, in Markdown and HTML alike, while a table or matrix view -stays a table. D2 writes tree, interconnection, state, action, sequence, -requirement, definition and package renderings, and refuses case and mixed -diagrams. Neither Graphviz, PlantUML nor D2 is needed to write a fence. -.PP -\-doc\-form html writes semantic HTML instead, carrying each element's -identity and kind, styled by a stylesheet in a cascade layer your own CSS -overrides without !important. -.PP -\-doc\-form pdf lays that HTML out with a print stylesheet through an external -converter named by \-pdf\-engine \(em weasyprint (default) or prince, or -pandoc, which reads the Markdown instead \(em run as a subprocess, never -linked in; diagrams are pre\-rendered to SVG with mermaid\-cli (mmdc). -\-html\-theme, \-html\-css and \-html\-no\-default\-css style the PDF as they -style the page. None of these tools is needed until PDF output is asked for; -scripts/download\-doc\-pdf\-toolchain.sh provisions pinned copies. -.PP -HTML output needs nothing external and loads nothing by default: \-html\-theme -picks one of the bundled looks (default, acm, ieee, modern, nasa, print, -report; acm, ieee and nasa follow those bodies' manuscript conventions), -\-html\-css adds your own stylesheets, \-html\-no\-default\-css drops the -default one, \-html\-fragment writes the document element alone to embed in a -page of yours, and \-html\-default\-css writes the default sheet out \(em or, -with \-html\-theme, a theme's whole sheet \(em to start from. \-html\-mermaid -cdn has the page load a pinned Mermaid release from jsDelivr so a browser -draws the Mermaid diagrams, or names a URL of your own to load it from. -.PP -Mathematics is LaTeX: a Span styled math is an inline formula, a Formula block -a display one. Markdown writes them between $ and $$ delimiters, HTML in \e( -\e) and \e[ \e] inside sysml\-math elements, left as source until \-html\-math -cdn has the page load a pinned MathJax release from jsDelivr, or a URL of your -own; PDF typesets them with KaTeX (katex) as it pre\-renders diagrams with -mermaid\-cli. -.SH FLAG ORDER -Flags may be written before or after the model they apply to. A file named -like a flag is read as a file after \-\-, which ends the flags: sysml \-trace -\-\- \-m.sysml -.SH READING FROM STANDARD INPUT -.RS 2 -.nf -cat model.sysml | sysml \-validate \- # A lone \- names standard input -cat model.sysml | sysml \- \-convert ttl \-from sysml -.fi -.RE -.PP -What was read from standard input is called in diagnostics, and a file -really named "\-" is read by naming it ./\- instead. -.SH PROFILING A RUN -.RS 2 -.nf -# Report what the run cost, on stderr -sysml \-validate \-memstats model.sysml -# Write a heap profile for go tool pprof -sysml \-validate \-memprofile heap.out model.sysml -# Write a CPU profile for go tool pprof -sysml \-validate \-cpuprofile cpu.out model.sysml -.fi -.RE -.SH EXIT STATUS -Every run that is not a prompt exits: -.TP -.B 0 -It did what was asked. -.TP -.B 1 -The model answered false for a check. -.TP -.B 2 -What was asked could not be carried out at all \(em an unreadable file, a -model that did not analyse cleanly, an unresolved name, a failed conversion. -.TP -.B 3 -Part of what was asked was carried out: a \-render\-documents set in which -some document could not be rendered. The others were written, a page stating -the error stands in for each that was not, and each failure is reported with -the document's qualified name. -.SH OUTPUT STREAMS -What was asked for is reported on stdout and what went wrong on stderr, -prefixed "sysml: " unless it locates a finding in the source. -.SH ENVIRONMENT -.TP -.B OPENSYSML_LIBRARY_PATH -Directory to load the SysML/KerML standard library from, instead of the copy -embedded in the binary. -.TP -.B OPENSYSML_MAX_STEPS -Expression evaluations one run may spend before it is reported as a runaway. -Default 10000000. -.TP -.B OPENSYSML_MAX_ACTION_STEPS -Token\-flow steps one action run may perform. Default 1000000. -.TP -.B OPENSYSML_MAX_EVENTS -Events one state machine run may dispatch. Default 1000000. -.TP -.B OPENSYSML_MAX_DO_STEPS -Do actions one state machine run may perform. Default 5000000. -.TP -.B OPENSYSML_MAX_ELEMENTS -Collection elements one evaluation may hold, which bounds the memory a run -holds rather than the work it does. Default 1000000. -.TP -.B OPENSYSML_MAX_CALC_DEPTH -Nested calc invocations one run may hold on the stack, which is what a -recursion spends. Default 10000, ceiling 25000. -.TP -.B OPENSYSML_MAX_SWEEP_RUNS -Runs one parameter sweep or sample may make, each a whole analysis or calc run -of its own with its own budgets. Default 1000. -.TP -.B OPENSYSML_MAX_INTEGER_BITS -Bits the magnitude of one Integer a run computes may take; Integers are -otherwise unbounded, and a result past it, such as a large power, is refused -before it is computed. At least 64, so every machine\-word Integer fits; -default 1048576. -.TP -.B OPENSYSML_JOBS -Runs of one check that may go concurrently \(em the linearizations of an -exploration, the engines \-engine all consults \(em each on a worker of its -own over the shared model, and files of one load that are parsed and validated -at once; \-jobs and %jobs override it. Default one per CPU, fewer where the -memory available leaves less than 512 MiB per worker. -.TP -.B OPENSYSML_TOOLS -Directory of the tool manifest: one JSON file per external tool (toolName, -version, executable, variables), each registered as the engine tool: -that runs actions and calcs annotated ToolExecution with that toolName. Unset -registers no tool, and such an action or calc is refused. -.TP -.B OPENSYSML_TOOL_TIMEOUT -How long one tool invocation may take, as a Go duration. Default 10s, after -which the performance or calculation fails. -.TP -.B OPENSYSML_SMT -Executable the %check, %explain, %solve, %configure and %optimize commands -drive as their SMT solver, speaking SMT\-LIB2 on standard input -(experimental). Unset looks for z3, then cvc5, on PATH. -.TP -.B OPENSYSML_SMT_TIMEOUT -How long one solver query may take, as a Go duration. Default 10s, after which -the verdict is unknown; a check's \-check\-timeout takes its place for the smt -engine's queries. -.TP -.B OPENSYSML_SMT_CORE_BUDGET -How long %explain may spend reducing an unsat core to a minimal one, as a Go -duration. Default 30s. -.TP -.B OPENSYSML_SMT_MAX_CONFIGURATIONS -How many variant selections %configure ... all may report before saying the -enumeration was cut short. Default 32. -.PP -Each variable above also answers to its legacy SYSML_\-prefixed name -(SYSML_MAX_STEPS for OPENSYSML_MAX_STEPS, and so on), which remains accepted. -When both are set and the OPENSYSML_ value is non\-empty, the OPENSYSML_ value -wins; setting only the legacy name prints a one\-time deprecation warning on -standard error. -.PP -A budget bounds one run \(em one evaluation, one instantiation, one calc -invocation, one action, one state machine \(em not a whole session, so a long -session of small operations never exhausts one. A run started inside another -shares the outer run's budget. -.SH FILES -.TP -.B $XDG_STATE_HOME/sysml/history -Prompt history, when XDG_STATE_HOME is set and writable. -.TP -.B ~/.sysml_history -Prompt history otherwise. When neither can be written the history is kept for -the session only. -.SH REPORTING BUGS -Report bugs at https://github.com/Open\-MBEE/OpenSysML/issues. -.SH SEE ALSO -sysml\-lsp(1), sysml\-grpc(1), https://github.com/Open\-MBEE/OpenSysML From 6a435f1aae05aa7681a60af517849229e919494e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:47:24 +0000 Subject: [PATCH 8/8] fix(cli): the v1 migration feature lists -portable among its flags; regenerate the manual page Co-Authored-By: jason.han --- cmd/sysml/features.go | 2 +- packaging/man/man1/sysml.1 | 1257 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1258 insertions(+), 1 deletion(-) diff --git a/cmd/sysml/features.go b/cmd/sysml/features.go index ec9732d9c2..82137d0e1f 100644 --- a/cmd/sysml/features.go +++ b/cmd/sysml/features.go @@ -53,7 +53,7 @@ func boolFlags(names ...string) []featureFlag { var ( v1Feature = &feature{ name: "v1", - flags: flags("migrate", "migration-report", "migration-results", "layout", "image-base-url"), + flags: append(flags("migrate", "migration-report", "migration-results", "layout", "image-base-url"), boolFlags("portable")...), values: map[string][]string{"from": {"xmi", "uml", "mdzip"}}, sections: []string{"Migration"}, markers: []string{"SysML v1"}, diff --git a/packaging/man/man1/sysml.1 b/packaging/man/man1/sysml.1 index e69de29bb2..3166bc9d2e 100644 --- a/packaging/man/man1/sysml.1 +++ b/packaging/man/man1/sysml.1 @@ -0,0 +1,1257 @@ +.\" Generated by "sysml -man"; edit the description in the command's usage.go. +.TH SYSML 1 "2026-08-30" "OpenSysML" "OpenSysML Manual" +.SH NAME +sysml \- run, check, convert and render SysML v2 and KerML models, and migrate SysML v1 +.SH SYNOPSIS +.B sysml +[options] [file...] +.SH DESCRIPTION +sysml loads the models it is given \(em a file, a directory to walk or a glob +\(em as a single model, so a declaration in one file resolves against the +others whichever order they were named in. With no expression or check to +carry out it opens an interactive prompt; otherwise it does what was asked and +exits on the verdict, which is what lets a run gate a build. +.SH OPTIONS +.SS General +.TP +.BR \-h ", " \-help +Show this help and exit +.TP +.BR \-v ", " \-version +Show the version and exit +.TP +.B \-man +Write this command's manual page, in roff, to stdout and exit +.SS Evaluating +.TP +.BR \-e ", " \-eval " \fIexpr\fP" +Evaluate this expression, against the model when one is loaded, and exit +(repeatable) +.TP +.BR \-query " \fItext\fP" +Evaluate this OSLC Query text against the model and exit +.SS Checking a model +.TP +.BR \-validate "[=\fIobject\fP]" +Report the model's diagnostics and exit, nonzero on an error; +\-validate= checks instead every assertion about that object +(repeatable) +.TP +.B \-strict +Judge the model as conforming SysML v2: notation no pinned production admits +is an error, not a warning; a SysML v1 migration writes none of it and refers +to no OpenSysML library +.TP +.BR \-disable\-lint " \fIcode\fP" +Leave this lint out of the model's diagnostics: undeclared\-signal, +port\-type\-mismatch, deferred\-keeper\-unmarked or rounded\-real\-literal, +comma\-separated or repeated +.TP +.BR \-enable\-lint " \fIcode\fP" +Report this opt\-in lint, off by default, in the model's diagnostics: +rounded\-real\-literal, comma\-separated or repeated; \-disable\-lint wins +.TP +.B \-no\-record\-cache +Parse every file loaded and hold it loaded, reading no interface record from +the record cache and writing none; default off, or OPENSYSML_RECORD_CACHE=0 +.TP +.BR \-constraint " \fIname\fP" +Evaluate this constraint and exit (repeatable) +.TP +.B \-self\-check +Apply the SysMLValidation constraints to every reflectively classified model +element and exit +.TP +.BR \-self\-check\-package " \fIpackage\fP" +Apply this package's constraint defs with the SysMLValidation ones, as +\-self\-check\-package Acme::ModelingRules; implies \-self\-check (repeatable) +.TP +.BR \-requirement " \fIname\fP" +Evaluate this requirement, and every verification case verifying it, and exit +(repeatable) +.TP +.BR \-satisfy "[=\fIname\fP]" +Evaluate every satisfaction assertion, or with \-satisfy= those the +named element states, and exit (repeatable) +.TP +.BR \-calc " \fIcall\fP" +Invoke this calculation and report its result, as \-calc "Fall(3, 4)" +(repeatable) +.TP +.BR \-analysis " \fIcall\fP" +Run this analysis or verification case and report its outputs and verdict, as +\-analysis "Pkg::Case(3.0) Pkg::part" (repeatable) +.TP +.BR \-tool\-dry\-run " \fIcall\fP" +Show what the external tool the case's or action's ToolExecution names would +be given \(em manifest, executable, argv, environment, standard input and +reply mapping \(em without starting it, and discards what the run did; +repeatable +.TP +.BR \-record\-run " \fIcall\fP" +Run this analysis case as \-analysis does and record the run into the model as +AnalysisRecords elements, one record per \-sweep value or \-runs run +(repeatable) +.TP +.BR \-record\-into " \fIpackage\fP" +Record \-record\-run runs into this package instead of a Records package +beside the case's +.TP +.BR \-run\-query " \fIquery\fP" +Execute this document query and report its rows, as \-run\-query "Heavy +root=telescope" (repeatable) +.TP +.BR \-instantiate " \fIname\fP" +Create an object of this definition or usage before the checks, so a verdict +is about it (repeatable) +.TP +.B \-json +Report checks as one JSON document rather than as lines +.SS Running behaviors +.TP +.BR \-action " \fIname\fP" +Run this action to completion, as \-action "Drive rover1" to run it on an +object (repeatable) +.TP +.BR \-state " \fIname\fP" +Run this state machine, as \-state "Mission rover1" to run it on an object +(repeatable) +.TP +.BR \-advance " \fItime\fP" +Simulated time units to run the \-action and \-state behaviors for, on one +shared clock; without it a state machine takes only its initial transition +under every schedule and engine +.TP +.BR \-schedule " \fIpolicy\fP" +Policy every run resolves its choice points under: declared, reverse +(default), seed:, explore[:runs=N,depth=D] or replay: +.TP +.BR \-seed " \fIn\fP" +Seed the model's own draws \(em weighted decisions, random functions \(em and +those of \-samples and \-runs; the same seed draws the same run or table +.TP +.BR \-draws " \fIpolicy\fP" +How every run resolves the draws of RandomFunctions: random (default) draws +from \-seed; min, max and average take each call's least, greatest or mean +value and need no seed; weighted decisions draw from \-seed whatever the +policy +.TP +.BR \-clock\-step " \fIseconds\fP" +The step, in seconds, the clock of every run ticks by: a wait comes due at the +first multiple of it not before the wait ends; 0 (default) is a continuous +clock. With \-compare\-results, replaces every configuration's stepSize +.TP +.BR \-runs " \fIn\fP" +Run the \-action, or the Simulation::MonteCarlo \-analysis, this many times, +each run seeded from \-seed, and table the \-observe features or the case's +observed value; needs \-seed unless \-draws is min, max or average +.TP +.BR \-observe " \fIfeature\fP" +Report this feature of the \-runs action, or clock for the time it completed +at; default every feature it holds and the clock. With \-compare\-results, the +stored observable to compare, or = to read it from +another feature of the run (repeatable) +.TP +.BR \-sweep " \fIrange\fP" +Run the \-analysis or \-calc once per value of this range, as \-sweep +"n=1..8:2"; several ranges run their cartesian product (repeatable) +.TP +.BR \-samples " \fIn\fP" +Draw this many values uniformly from each \-sweep range instead of stepping +through it; needs \-seed +.TP +.BR \-jobs " \fIn\fP" +Runs of one check that may go concurrently, each on a worker of its own, and +files of one load that are parsed and validated at once; the result is the +same at any count. Default OPENSYSML_JOBS, else one per CPU, fewer where the +memory available leaves less than 512 MiB per worker +.SS Analysis engines +.TP +.B \-engines +List the analysis engines this build knows and their status, spawning nothing, +and exit +.TP +.B \-probe +With \-engines, also start each external engine once and report the outcome as +its status +.TP +.BR \-engine " \fIname\fP" +Analysis engine every check is put to: auto (default), all, or one by name +\(em run, explore, check, smt, sweep, solve, tool: or an external engine +.SS Checking every schedule (\-engine check, smt or all) +.TP +.BR \-check\-property " \fIname\fP" +Evaluate this constraint or requirement at every stable state of the action +and report a schedule at which it is false (repeatable) +.TP +.BR \-check\-diverge " \fIfeature\fP" +Report this feature sensitive when schedules leave it with different final +values, as x, step.out or this.level; default every attribute (repeatable) +.TP +.BR \-check\-input " \fIfeature\fP" +Under smt, leave this feature of the action free in its declared domain +although the model binds it (repeatable) +.TP +.BR \-check\-assume " \fIname\fP" +Under smt, assume this constraint or requirement over the initial state of the +action (repeatable) +.TP +.BR \-check\-witness " \fIdir\fP" +Write a witness file for each violation and each divergent value to this +directory, for \-schedule replay: and %replay to follow +.TP +.BR \-check\-depth " \fIn\fP" +The most moves one schedule may make before the search backtracks, or the +moves smt unrolls the action to (default 10000 for check, 40 for smt) +.TP +.BR \-check\-states " \fIn\fP" +Under check, the most distinct states the search may visit (default 1000000) +.TP +.BR \-check\-unroll " \fIn\fP" +Under smt, the most iterations of one loop the solver unrolls before it stops +(default 4) +.TP +.BR \-check\-timeout " \fIduration\fP" +The time the check's plan may run for, as 30s or 2m, and the time each smt +solver query may take in place of OPENSYSML_SMT_TIMEOUT +.SS Converting and migrating +.TP +.BR \-convert " \fIformat\fP" +Convert the model to this format instead of running it: sysml, kerml, ttl, +turtle, rdf or api\-json (RDF and the API element form are experimental). The +input may be a Flexo branch URL (host[:port][/base]/projects/{p}/branches/{b} +of the FLEXO_SYSMLV2_URL endpoint, or flexo://{p}/{b}), read as its RDF graph. +A SysML v1 model is refused: it is migrated, with \-migrate +.TP +.BR \-migrate " \fIformat\fP" +Migrate the SysML v1 model (.xmi, .uml or .mdzip; \-from names the format when +the extension does not) to SysML v2, written in this format: sysml, kerml, +ttl, turtle, rdf or api\-json. Ledgered, not lossless: every element is +mapped, approximated or left unmapped, and \-migration\-report says which +(experimental) +.TP +.BR \-from " \fIformat\fP" +Input format for \-convert: sysml, kerml, ttl, turtle, rdf, api\-json, or fmu +for a Functional Mock\-up Unit to import; for \-migrate: xmi, uml or mdzip, +the forms of a SysML v1 model; default the input's extension +.TP +.BR \-import " \fIfile\fP" +Set the feature values a CSV, TSV, JSON or JSON Lines file assigns, one row +per element, before \-convert writes the model; an element column names the +element and each other column a feature, as mass [kg] (repeatable) +.TP +.BR \-import\-as " \fIshape\fP" +What \-import makes of each row: values, which sets the features of the +element the row names. The default is values. +.TP +.BR \-import\-map " \fIfile\fP" +Read the \-import before it onto elements and features as this JSON mapping +file says +.TP +.BR \-import\-format " \fIformat\fP" +Read the \-import before it as csv, tsv, json or jsonl rather than as its +extension says +.TP +.B \-import\-dry\-run +Report the values \-import would set, changing and writing nothing +.TP +.BR \-id " \fIname\fP" +With \-convert ttl or api\-json, how derived element ids are spelled: +qualified (default) derives each from its qualified name; uuid mints +name\-based uuids under each root package, as the library convention does +.TP +.BR \-o ", " \-output " \fIfile\fP" +Write what \-convert, \-migrate, \-compile, \-render or \-render\-document +produces to this file instead of stdout; with \-convert ttl or \-migrate ttl, +a Flexo branch URL pushes the graph to the branch +.TP +.BR \-migration\-report " \fIfile\fP" +With \-migrate, write the element\-by\-element migration report to this file: +JSON when it ends in .json, text otherwise +.TP +.BR \-migration\-results " \fIfile\fP" +With \-migrate, write the run configurations and the result snapshots the +simulation tool stored for them to this JSON file, for \-compare\-results to +read against the migrated model +.TP +.BR \-layout " \fIfile\fP" +With \-migrate, read this MTIP export (HUDS XML) and write the diagram +geometry it records as DiagramLayout annotations in the migrated views +.TP +.BR \-image\-base\-url " \fIurl\fP" +With \-migrate, the http(s) URL a comment's relative is resolved +against, such as the View Editor server +.TP +.B \-portable +With \-migrate, append the OpenSysML library packages the output refers to, so +the one file loads in a tool that ships only the standard library; the report +names them +.TP +.BR \-compare\-results " \fIfile\fP" +Run every configuration this \-migration\-results file indexes \(em or those +\-action names \(em with its recorded runs and duration mode, or the \-runs +and \-draws given, seeded from \-seed, and table the tool's and OpenSysML's +min, mean, p50, p90 and max of each observable with their relative difference +.SS Compiling natively +.TP +.BR \-compile " \fIcalc\fP" +Compile this calc def to a native executable named by \-o, as \-compile +Pkg::Fib +.TP +.BR \-target " \fIbackend\fP" +Backend \-compile generates code for: c or go. The default is c. +.TP +.B \-source +With \-compile, write the generated source to \-o instead of building it +.SS Rendering views +.TP +.BR \-render " \fIview\fP" +Render this view of the model instead of running it, in the form its render +member states; # renders every file loaded and #: one +element, kind being tree, interconnection, state, action, case, mixed, +sequence, table or matrix, without a declared view +.TP +.BR \-render\-all " \fIdir\fP" +Render every declared view into this directory +.TP +.BR \-graphs " \fIname\fP" +Write the lowered graph of this action or state machine, and of every behavior +it performs, as the canonical graphs:1 JSON an external analysis engine is +sent, to stdout or \-output +.TP +.BR \-render\-form " \fIform\fP" +Form \-render or \-render\-all writes: text, mermaid, markdown, dot, plantuml, +d2, csv or tsv (csv and tsv for a table or matrix); D2 writes tree, +interconnection, state, action, sequence, requirement, definition and package +renderings, not case or mixed; default from the destination for \-render, each +kind's machine form for \-render\-all +.TP +.BR \-render\-palette " \fIpalette\fP" +Palette the dot, mermaid, plantuml or d2 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 +.TP +.BR \-render\-link " \fItemplate\fP" +Source link template for \-render, \-render\-all, \-render\-document, +\-render\-documents and \-render\-run. Placeholders: {file}, {line}, {col}, +{qname}, {id}; {file} is the path as loaded (use absolute paths for vscode:// +or file:// links) +.TP +.BR \-render\-unplaced " \fIplacement\fP" +Where a graph form of a view some Layout positions puts the nodes none does: +omit (default) leaves them undrawn in every form, strip draws them, in rows +below the dot drawing; applies to \-render, \-render\-all and document +diagrams +.TP +.BR \-render\-style " \fIstyle\fP" +Drawing style of the dot or mermaid form: pilot (default), the Pilot +visualizer's black and white, or cameo, the look of Cameo Systems Modeler; +applies to \-render, \-render\-all and document diagrams +.TP +.BR \-render\-ports " \fIdisplay\fP" +How much of a part's ports \-render or \-render\-all draws on an +interconnection or mixed rendering: minimal (default), the ports its +interconnection edges end at, each a small square on the part's border named +beside it, or full, every port, labelled name : Type +.TP +.BR \-render\-overlay " \fIoverlay\fP" +What \-render or \-render\-all draws over a requirement rendering's structure: +verdicts runs the verification cases verifying each requirement and colours +and labels it by their verdicts; default none, a purely structural drawing +.SS Listing documents and views +.TP +.BR \-list " \fIwhat\fP" +List the model's documents, views, diagrams (the graph\-shaped views), +pseudo\-views or all of them, by the qualified names \-render\-document and +\-render read: documents, views, diagrams, pseudo\-views or all +.TP +.BR \-list\-kind " \fIkinds\fP" +Comma\-separated view kinds \-list views, diagrams or all keeps, as +state,action +.TP +.BR \-list\-form " \fIform\fP" +Form \-list writes: text (default), one aligned line per item, tsv or json +.SS Rendering documents +.TP +.BR \-render\-document " \fIname\fP" +Compile this document definition, run its queries and write the rendered +document +.TP +.BR \-render\-documents " \fIdir\fP" +Render every document definition, linked to one another, into this directory; +a document that cannot be rendered gets a page stating why and the run exits 3 +.TP +.BR \-doc\-form " \fIform\fP" +Form the documents are written in: markdown (default), html or pdf, which +drives an external converter +.TP +.BR \-diagram\-form " \fIform\fP" +Form the documents' graph\-shaped diagrams are written in: mermaid, dot, +plantuml or d2; D2 writes tree, interconnection, state, action, sequence, +requirement, definition and package renderings, not case or mixed; unset, a +positioned view is dot and any other mermaid; a table or matrix view is a +table either way +.TP +.B \-doc\-title\-page +Put the document title on a page of its own (html or pdf) +.TP +.B \-doc\-toc +Write a table of contents ahead of the content (html or pdf) +.TP +.B \-doc\-number\-sections +Number the section headings hierarchically (html or pdf) +.TP +.B \-doc\-number\-figures +Number the figures and tables in their captions, Figure 1. and Table 1. in +document order (markdown, html or pdf) +.TP +.BR \-pdf\-engine " \fIconverter\fP" +Converter \-doc\-form pdf drives: weasyprint (default), pandoc or prince +.SS Styling HTML documents +.TP +.BR \-html\-theme " \fItheme\fP" +Style the HTML page or PDF with a bundled theme layered over the default +stylesheet: default, acm, ieee, modern, nasa, print or report +.TP +.BR \-html\-css " \fIfile|url\fP" +Style the HTML or PDF with this stylesheet too: a file is inlined, a URL is +linked (repeatable, applied in order after the default sheet) +.TP +.B \-html\-no\-default\-css +Leave the default stylesheet out, so only \-html\-css sheets style the HTML or +PDF +.TP +.B \-html\-fragment +Write the document element alone, without the page shell or a stylesheet, to +embed in a page of your own +.TP +.B \-html\-default\-css +Write the default document stylesheet, or with \-html\-theme that theme's +whole sheet, and exit +.TP +.BR \-html\-mermaid " cdn|\fIurl\fP" +Have the page load Mermaid to draw its diagrams: cdn loads a pinned release +from jsDelivr, a URL the script it names +.TP +.BR \-html\-math " cdn|\fIurl\fP" +Have the page load MathJax to typeset its formulas: cdn loads a pinned release +from jsDelivr, a URL the script it names +.SS Syncing against a repository +.TP +.BR \-sync\-diff " \fIrepo\fP" +Show the change set between the model and this repository \(em a graph file +(.ttl) or a SysML v2 API endpoint URL \(em and exit; never writes +.TP +.BR \-sync\-apply " \fIurl\fP" +Apply the change set to the model's project branch at this SysML v2 API +endpoint URL, then record the commit in the sync state +.TP +.BR \-sync\-base " \fIfile\fP" +Repository graph at the last\-seen commit; with it, repository changes since +then surface as conflicts +.TP +.BR \-sync\-state " \fIfile\fP" +Sync state file recording project, branch and last\-seen commit; default +.sync.json beside the model +.TP +.B \-sync\-confirm\-deletes +Confirm repository\-side deletes; without it the diff reports them but +applying is refused +.TP +.B \-sync\-mint\-ids +Mint a UUID for each unannotated element being created, so the repository can +address it stably +.TP +.BR \-sync\-annotate " \fIfile\fP" +Write the model to this file with each minted id declared as an @ElementId +annotation (needs \-sync\-mint\-ids) +.SS Diagnostics and profiling +.TP +.B \-debug +Report every diagnostic over the whole session buffer, with the pass that +produced it +.TP +.B \-quiet +Report errors only, suppressing warnings +.TP +.B \-trace +Report each execution step: expression evaluation, calc invocation, action +tokens, state transitions +.TP +.BR \-render\-run " \fIkind\fP=\fIpath\fP" +Render the trace of a run as timeline or sequence into path (\- is stdout); +form comes from \-render\-form or the path extension +.TP +.BR \-cpuprofile " \fIfile\fP" +Write a CPU profile of the run to this file, for go tool pprof +.TP +.BR \-memprofile " \fIfile\fP" +Write a heap profile of the run to this file, for go tool pprof +.TP +.B \-memstats +Report on stderr what the run cost: wall time, memory allocated, memory taken +from the OS +.SS Deprecated +.TP +.BR \-to " \fIformat\fP" +Replaced by \-convert, which names the output format +.TP +.B \-pdf\-title\-page +Former name of \-doc\-title\-page, which also shapes HTML +.TP +.B \-pdf\-toc +Former name of \-doc\-toc, which also shapes HTML +.TP +.B \-pdf\-number\-sections +Former name of \-doc\-number\-sections, which also shapes HTML +.SH EXAMPLES +.RS 2 +.nf +sysml # Start interactive REPL +sysml \-e "5 + 3" # Evaluate and exit +sysml \-e "expr" file.sysml # Load file, evaluate, and exit +sysml file.sysml # Load file and start REPL +sysml \-debug file.sysml # Load file, reporting every diagnostic +sysml \-trace file.sysml # Load file, reporting each execution step +.fi +.RE +.SH CHECKING A MODEL +.RS 2 +.nf +# Evaluate one constraint and exit +sysml \-constraint MassBudget model.sysml +# Evaluate one requirement and exit +sysml \-requirement PowerMargin model.sysml +# Evaluate every satisfaction assertion +sysml \-satisfy model.sysml +# \&...only the ones Ctx states +sysml \-satisfy=Ctx model.sysml +# Check C against an object of p +sysml \-instantiate p \-constraint C model.sysml +# Report diagnostics only +sysml \-validate model.sysml +# \&...asking whether it is conforming SysML v2 +sysml \-validate \-strict model.sysml +# Apply the SysMLValidation constraints +sysml \-self\-check model.sysml +# \&...plus a rule package's constraints +sysml \-self\-check\-package Acme::Rules m.sysml +# Check every assertion about an object +sysml \-instantiate car \-validate=car m.sysml +# Invoke a calculation +sysml \-calc "Fall(3, 4)" model.sysml +# Run an analysis case +sysml \-analysis shipCost model.sysml +# \&...on an object as its subject +sysml \-analysis "CostAnalysis ship" model.sysml +# Run a verification case for its verdict +sysml \-analysis speedCheck model.sysml +# Execute a document query +sysml \-run\-query "Heavy root=scope" model.sysml +# Run an action to completion +sysml \-action Drive model.sysml +# Run a state machine for 10 time units +sysml \-state Mission \-advance 10 model.sysml +# \&...an action and a machine on one clock +sysml \-action Ping \-state Sm \-advance 5 m.sysml +# Run every linearization; table the outcomes +sysml \-schedule explore \-action Drive m.sysml +# Report the verdicts as JSON +sysml \-satisfy \-json model.sysml +.fi +.RE +.PP +Check flags may be combined. \-validate= checks every assertion about +an object \-instantiate created and the objects it holds: an object named, +#, or a path such as car.engine. \-self\-check applies the OpenSysML +SysMLValidation constraints to each reflectively classified element; underived +reflective features are reported as unevaluated, not as violations. +\-self\-check\-package adds the constraint defs of a package the loaded files +declare to the same walk, as \-self\-check\-package Acme::ModelingRules: a +rule is a constraint def whose first in parameter is typed by a SysML or KerML +metaclass, and it applies to every element that metaclass conforms to. +\-requirement and \-satisfy report beside each verdict the verdict of every +verification case verifying the requirement. \-analysis takes arguments for +the case's inputs and an object as its subject, as \-analysis "Pkg::Case(3.0) +Pkg::part"; a verification case also reports the verdict its body produced. +.SH RUNNING ACTIONS AND STATE MACHINES +\-action runs an action to completion and \-state a state machine, both on one +shared clock that \-advance runs for the given simulated time units; without +\-advance a state machine takes only its initial transition. Either names a +definition or usage, or one followed by the object to perform it on, as +\-action "Drive rover1". +.PP +\-schedule is the policy every run resolves its choice points under \(em +concurrent tokens, overlapping guards, competing transitions: declared, +reverse (the default), seed: for a reproducible pseudo\-random order, +explore[:runs=N,depth=D] to run every linearization within the budget and +table the distinct outcomes, or replay: to follow a witness file's +choice lines move for move, then reverse. +.PP +Under \-schedule explore, \-engine check, smt or all, each run creates objects +of its own before the behaviors start: one per \-instantiate, which a \-state +or \-action named alone attaches to, and one per \-action or \-state naming a +definition or usage to create or a path into one such as mission.rover. The +declaration is created once per run, so machines on sibling parts share it and +its connectors. +.PP +\-jobs is how many runs of one check may go concurrently \(em the +linearizations of an exploration, the engines \-engine all consults \(em each +on a worker of its own over the shared model, and how many files of one load +are parsed and validated at once; the result is the same at any count. By +default one per CPU, fewer where the memory available leaves less than 512 MiB +per worker. +.SH ANALYSIS ENGINES +.RS 2 +.nf +# List engines, kind and status; nothing runs +sysml \-engines +# Also start each external engine once +sysml \-engines \-probe +# The same as \-schedule explore +sysml \-engine explore \-action Drive m.sysml +# Every engine that covers the question +sysml \-engine all \-requirement R m.sysml +# One engine; the plan in the report +sysml \-engine run \-json \-constraint C m.sysml +.fi +.RE +.PP +Every check is a question put to an analysis engine: run executes the model +once under \-schedule, explore runs every linearization, sweep runs the rows +of \-sweep, and solve puts condition sets to an SMT solver. \-engine auto (the +default) picks the engine of highest authority that covers the question and +falls back to the next when it refuses or covers nothing; \-engine puts +the question to that engine alone, and its refusal is the answer; \-engine all +puts it to every engine that covers it, one after another in name order, and +composes their answers: a witnessed violation stands over any universal claim, +and a contradiction is reported as a disagreement decided in the interpreter's +favor. +.PP +Every verdict is followed by its standing: the claim, the strength of the +evidence \(em observed (one run), witnessed (a replayed execution), bounded +(every case within a budget), proved (every case) \(em and what earned it, as +`holds (observed: 1 run under reverse)`. A budget the engine reached lowers +the strength and is named. Under \-json each check carries the plan and one +results[] entry per engine that answered, with its engine, claim, strength, +bounds and witness. +.PP +\-engines lists each engine's name, kind (built\-in, tool, engine, policy, +sampler), protocol, authority, the questions it answers and whether its +process is found, with the manifest entry and resolved command of each +external one; \-probe also starts each external engine once and checks its +describe against its manifest entry field by field. An engine is named run, +explore, check, smt, sweep, solve, tool: from OPENSYSML_TOOLS, or as an +external engine from OPENSYSML_ENGINES. \-engine explore is \-schedule +explore; \-engine check searches every schedule of each \-action for a +violation, deadlock, failure or divergence; \-engine smt decides each +\-check\-property over every schedule and every value of the action's free +inputs through an SMT solver. +.SH CHECKING EVERY SCHEDULE +.RS 2 +.nf +# Search every schedule for Safe +sysml \-engine check \-action Go \-check\-property Safe m.sysml +# Is speed schedule\-dependent? +sysml \-engine check \-action Go \-check\-diverge speed m.sysml +sysml \-engine smt \-action Go \-check\-input t \-check\-property Safe \e + m.sysml +# A witness file per finding +sysml \-engine check \-action Go \-check\-witness out m.sysml +.fi +.RE +.PP +\-check\-property evaluates a constraint or requirement at every stable state +of the action, on the performing object when there is one, and reports a +schedule at which it is false. \-check\-diverge reports a feature sensitive +when schedules leave it with different final values \(em check searching the +schedules, smt asking the solver for two that end it apart \(em named as x, +step.out for a performed node's output or this.level for the performing +object's (not covered under smt); a name nothing holds is refused, and without +it every attribute of the action and of its performing object is checked, the +action's own when it has none. +.PP +Under smt every input the model leaves unbound is free and every bound one is +pinned; \-check\-input leaves a bound feature free in its declared domain, and +a name that is not a feature the action reads is refused. \-check\-assume +assumes a constraint or requirement over the initial state; a set no initial +state satisfies is reported not covered, never proved. +.PP +\-check\-witness writes a file for each violation and each divergent value +\(em the inputs the solver chose, the schedule's choices, a blank line, then +the run's trace \(em which \-schedule replay: and %replay follow. A +bound is named when it is hit: \-check\-depth is the most moves one schedule +may make before the search backtracks, or the moves smt unrolls the action to; +\-check\-states the most distinct states the search may visit, the same figure +\-engine all gives an exploration as its runs; \-check\-unroll the most +iterations of one loop smt unrolls; \-check\-timeout the time the plan may run +for and the time each solver query may take in place of OPENSYSML_SMT_TIMEOUT +\(em a search it stops is reported incomplete with the states and depth +reached, not as a verdict. +.SH SWEEPING AND SAMPLING A PARAMETER +.RS 2 +.nf +# One run per range value +sysml \-calc Twice \-sweep "n=1..8:2" m.sysml +# Every combination of the two +sysml \-analysis C \-sweep "m=1..3" \-sweep "v=1..2" m.sysml +# 20 values drawn uniformly +sysml \-analysis C \-sweep "m=1..9" \-samples 20 \-seed 7 m.sysml +# Report the rows as JSON +sysml \-analysis C \-sweep "m=1..3" \-json m.sysml +.fi +.RE +.PP +A sweep is orchestration over the ordinary run: each row is one \-analysis or +\-calc invocation with the swept parameter bound to that row's value and every +other argument as given, so nothing about how a case executes changes. Name a +single \-analysis or \-calc; a parameter the case does not declare, one the +invocation already binds, a step of zero or one pointing away from the end of +the range is an error rather than a silent row. +.PP +An endpoint and a step carry the syntax and the units an argument carries +("mass=0.0 [SI::kg]..9.0 [SI::kg]:3.0 [SI::kg]"), and the end of the range is +included where the step lands on it. +.PP +The values are produced in the swept parameter's declared type, not the +literals': a Real parameter swept over 1..4:1 is bound to 1.0, 2.0, 3.0, 4.0 +and an Integer one swept over 1.0..3.0:1.0 to 1, 2, 3, while an endpoint or +step no Integer parameter can take (:0.5) is refused before any run, as is a +range over a Boolean, String, enumeration or non\-scalar parameter. A +parameter declaring no type takes the range as written, and the table says so. +.PP +A range between whole numbers steps by one where no : is written; one +with a fractional endpoint needs one. A range read as reals takes an Integer +endpoint or step only where a Real holds it without rounding, and steps only +where the reals tell its rows apart. Rows come out in the order the ranges +were given, the first varying slowest, and a run that failed is a row carrying +its error rather than the end of the table. +.PP +\-samples draws that many values uniformly from each range instead of stepping +through it \(em Integers inclusively for a parameter taking Integers, reals in +[, ) for one taking reals \(em and needs \-seed: the same seed draws +the same table on every platform. Uniform is the only distribution on offer +\(em the bundled library states no probability distributions \(em so a range +must be written .. rather than as a named distribution. +.SH RUNNING AN ACTION MANY TIMES +.RS 2 +.nf +# 100 runs, every feature +sysml \-action Acquire \-runs 100 \-seed 7 m.sysml +# One observable +sysml \-action Acquire \-runs 100 \-seed 7 \-observe elapsed m.sysml +# One run, its draws seeded +sysml \-action Acquire \-seed 7 m.sysml +# Durations at their max +sysml \-action Acquire \-draws max m.sysml +# Mean durations +sysml \-action A \-runs 9 \-seed 7 \-draws average m.sysml +# A MonteCarlo analysis case +sysml \-analysis "Mc obj" \-runs 100 \-seed 7 m.sysml +.fi +.RE +.PP +\-runs runs one \-action to completion that many times, each run on a fresh +context whose modeled randomness \(em the weighted decisions @Probability +states and the draws of uniform, uniformInteger, triangular and normal \(em is +seeded from a seed of its own derived from \-seed, so the same seed makes the +same table on every platform and a run can be replayed alone. \-schedule stays +the second, independent knob: it decides the concurrency choices, which carry +no probability, in every run alike. +.PP +\-draws is the third knob, the duration policy: random (the default) draws +every RandomFunctions call from the seed; min, max and average resolve each +call to the least, greatest or mean value of its distribution instead \(em the +midpoint of a uniform, the mean of a triangular \(em so a run whose only +randomness is durations completes deterministically and needs no \-seed. +normal has no least or greatest value, so a run that calls it under min or max +stops with an error. Weighted decisions are not durations: they draw from the +seed under every policy, and an unseeded run takes the most probable branch. +The policy is written into every witness as `draws by `, and +\-schedule replay: follows it. +.PP +The table has one row per run, numbered, with each \-observe feature of the +action \(em `clock` is the simulation time the run completed at, never a +feature of that name \(em and without \-observe every feature the action holds +and the clock. Below it each numeric observable is summarised over the +completed runs: min, mean, max, the nearest\-rank p50 and p90, and a +histogram; a non\-numeric one is counted by value. A feature the action does +not hold is refused. +.PP +\-runs also runs an \-analysis that specializes Simulation::MonteCarlo, the +OpenSysML library's analysis of repeated runs: each run performs the case's +steps on a fresh subject, seeded as an action's run is, and reads the value +the case binds as observed; the table has one row per run with that value, and +beneath its distribution the case is concluded once over the sample \(em runs, +mean, deviation (the sample standard deviation) and outOfSpec (the runs in +which a check of the case did not hold) bound, and the case's own outputs and +checks evaluated over them. Each run makes its objects from their +declarations, so a subject named by `#id` alone is refused. Run once, without +\-runs, such a case leaves its statistics unbound. \-observe belongs to an +action's runs, not a case's. +.SH CONVERSION +.RS 2 +.nf +# SysML notation to RDF Turtle, on stdout +sysml model.sysml \-convert ttl +# RDF Turtle to SysML notation +sysml model.ttl \-convert sysml +# Write the conversion to a file +sysml model.sysml \-convert ttl \-o m.ttl +# Name the input format explicitly +sysml in.txt \-convert ttl \-from sysml +# A Flexo branch as notation +sysml flexo://demo/main \-convert sysml +# Push the graph to the branch +sysml model.sysml \-convert ttl \-o flexo://demo/main +.fi +.RE +.PP +The input format is taken from the file extension (.sysml, .kerml, .ttl, +\&.json, .fmu) unless \-from names it: sysml, kerml, ttl, turtle, rdf, +api\-json, or fmu for a Functional Mock\-up Unit to import. Converting to the +format it is already in rewrites the input: notation is reformatted, Turtle is +normalized. +.PP +A SysML v1 model (.xmi, .uml, .mdzip) is refused: it is migrated, not +converted; see Migration. +.PP +Either side may name a Flexo MMS project branch instead of a file: +http(s)://host[:port][/base]/projects/{project}/branches/{branch}, or +flexo://{project}/{branch}, both naming the endpoint FLEXO_SYSMLV2_URL +configures. A branch input is read as its head commit's RDF graph; a branch +\-o takes the \-convert ttl output as the branch's whole model graph, +conditional on the branch's etag, and refuses a head the sync state says has +moved. Both need the bearer token in FLEXO_INTEROP_TOKEN and record the head +commit in the sync state (\-sync\-state, or .sync.json on a read / +.sync.json on a push). +.PP +RDF conversion \(em Turtle and the API's JSON element form alike \(em is +experimental: the mapping covers model structure and the behavior its bodies +state, refuses what it cannot write back, and its vocabulary may change +without a compatibility path; see docs/reference/rdf\-mapping.md \(sc Status +.PP +Every run that converts RDF says so on stderr. Saving to .sysml or .kerml is +stable. +.SH MIGRATION +.RS 2 +.nf +# A Cameo project as notation +sysml Model.mdzip \-migrate sysml \-o Model.sysml +sysml Model.mdzip \-migrate sysml \-o Model.sysml \-migration\-report \e + Model.report.txt +# A v1 XMI export as RDF Turtle +sysml Model.xmi \-migrate ttl \-o Model.ttl +# Views laid out from MTIP +sysml Model.mdzip \-migrate sysml \-layout Model_mtip.xml +# Index the tool's results +sysml Model.mdzip \-migrate sysml \-migration\-results runs.json +# Then compare with the tool's +sysml Model.sysml \-compare\-results runs.json +# Name the v1 form explicitly +sysml export.xml \-from xmi \-migrate sysml +.fi +.RE +.PP +A SysML v1 model \(em UML XMI 2.5.1 with the SysML profile applied, an Eclipse +UML2 .uml file, or a Cameo/MagicDraw .mdzip archive \(em is migrated to SysML +v2, not converted: the migration is ledgered, not lossless. Every v1 element +gets one of three verdicts, mapped (written as the v2 construct it corresponds +to), approximated (written as the nearest v2 construct, with a note saying +what differs) or unmapped (left behind, with the reason); profile, library and +notation content nothing refers to is skipped. The one\-line summary of the +verdicts goes to stderr; \-migration\-report writes the element\-by\-element +ledger to a file, JSON when it ends in .json, text otherwise. \-convert +refuses a v1 model for this reason. +.PP +\-from names the v1 form (xmi, uml or mdzip) when the extension does not; +\-migrate writes sysml, kerml, ttl, turtle, rdf or api\-json, and refuses an +input that is not v1, which \-convert handles. \-layout names an MTIP export +of the same project, whose diagram geometry is written into the migrated views +as DiagramLayout metadata; \-image\-base\-url resolves the relative +of a comment to the server serving it; \-migration\-results writes the +simulation tool's run configurations and the result snapshots it stored as a +JSON sidecar, which \-compare\-results on the migrated model runs against; +\-strict writes only notation a pinned SysML v2 production admits, reporting a +construct whose only v2 form is an OpenSysML extension as unmapped. The images +a document's Image blocks carry are written beside \-o, so a migration that +wrote any needs \-o a local file path. +.PP +SysML v1 migration is experimental: the mapping covers structure, ports and +connectors, requirements, constraints, instances and allocations, reports +every element it approximates or leaves behind, and what it writes for a v1 +element may change without a compatibility path; see +docs/reference/sysml\-v1\-migration.md \(sc Status +.PP +Every run that migrates a v1 model says so on stderr. +.SH NATIVE COMPILATION +.RS 2 +.nf +# Compile a calc def to a C executable +sysml model.sysml \-compile Pkg::Fib \-o fib +# \&...via Go +sysml model.sysml \-compile Pkg::Fib \-target go \-o fib +# Write the generated source only +sysml model.sysml \-compile Pkg::Fib \-source \-o fib.c +.fi +.RE +.PP +The executable takes the calc's parameters as arguments and prints its result; +it computes what sysml \-calc computes, or fails with the same error. Only the +scalar subset compiles (Integer, Real, Boolean; see +docs/project/native\-compilation.md). +.SH SYNCING AGAINST A REPOSITORY +A dry run unless \-sync\-apply: +.PP +.RS 2 +.nf +# Show the change set and exit +sysml model.sysml \-sync\-diff repo.ttl +sysml model.sysml \-sync\-diff repo.ttl \-sync\-base last\-seen.ttl +sysml model.sysml \-sync\-diff repo.ttl \-sync\-confirm\-deletes +sysml model.sysml \-sync\-diff repo.ttl \-sync\-mint\-ids \-sync\-annotate \e + out.sysml +# Against the live API; no writes +sysml model.sysml \-sync\-diff http://localhost:8083 +# Write the change set as a commit +sysml model.sysml \-sync\-apply http://localhost:8083 +.fi +.RE +.PP +The diff correlates elements by their effective id \(em an @ElementId +annotation, or the encoded qualified name \(em so a rename, move or retype is +an update, never a delete plus a create. Repository\-only elements are +reported as deletes but applying them needs \-sync\-confirm\-deletes; +conflicts \(em a declared id the branch no longer has, or a repository change +since the last\-seen commit \(em exit 1 and are never resolved silently. +.PP +The last\-seen commit is tool state in .sync.json (or \-sync\-state), +never written into the notation. \-sync\-apply refuses a change set the dry +run would have flagged, sends each update under its retained id, and records +the resulting commit; the token comes from FLEXO_INTEROP_TOKEN. +.SH RENDERING A VIEW +.RS 2 +.nf +# ASCII text at a terminal +sysml model.sysml \-render Views::vehicleView +sysml model.sysml \-render Views::vehicleView \-render\-form markdown +sysml model.sysml \-render Views::vehicleView \-o view.mmd +sysml model.sysml \-render Views::vehicleView \-render\-form dot +sysml model.sysml \-render Views::vehicleView \-render\-form mermaid \e + \-render\-palette okabe\-ito +# unpositioned nodes in a strip below +sysml model.sysml \-render Views::vehicleView \-render\-form dot \e + \-render\-unplaced strip +# drawn in Mermaid's Cameo approximation +sysml model.sysml \-render Views::vehicleView \-render\-form mermaid \e + \-render\-style cameo +# drawn as Cameo draws it +sysml model.sysml \-render Views::vehicleView \-render\-form dot \e + \-render\-style cameo +# every port of every part, typed +sysml model.sysml \-render Views::loopView \-render\-form dot \e + \-render\-ports full +sysml model.sysml \-render Views::vehicleView \-render\-form plantuml \-o \e + view.puml +sysml model.sysml \-render Views::vehicleView \-render\-form d2 \-o view.d2 +sysml model.sysml \-render Views::partsTable \-render\-form csv \-o \e + parts.csv +# several files, loaded as one model +sysml types.sysml model.sysml \-render Views::vehicleView +sysml model.sysml \-render\-all rendered +.fi +.RE +.PP +The rendering is the one the view's render member states, and a containment +tree where it states none. It is tool\-defined output: SysML v2 specifies the +notation, not how a tool draws it. Notices \(em an empty view, an element the +rendering cannot represent \(em go on stderr. Every file named is loaded as +one model, so a view may expose elements a sibling file declares. +\-render\-form names the form written \(em text, mermaid, markdown, dot, +plantuml, d2, csv or tsv; by default \-render takes it from the destination +and \-render\-all writes each kind's machine form, one file per view named by +its qualified name with :: as . and every byte unsafe in a filename (/, \e, :, +\&., %, control characters, what Windows reserves) as %XX; a name past 255 bytes +is cut and tagged ~ and a hash of the whole. A graph\-shaped rendering +defaults to Mermaid. DOT writes tree, interconnection, state, action, case, +mixed, requirement, definition and package renderings; PlantUML writes those +kinds and sequence. D2 writes tree, interconnection, state, action, sequence, +requirement, definition and package renderings; case and mixed renderings +refuse D2 with a typed error. Table and matrix renderings refuse graph forms +with a typed error. Neither Graphviz, PlantUML nor D2 is needed to write graph +forms. A table or matrix is written as a Markdown table by default, and as +comma\- or tab\-separated values with \-render\-form csv or tsv: a header +record of the columns, then a record per row, quoted as RFC 4180 quotes a +field. Graph forms are drawn in the black\-and\-white style of the SysML v2 +Pilot visualizer; \-render\-palette fills the DOT, Mermaid, PlantUML and D2 +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. \-render\-style names the +look the DOT and Mermaid forms draw in: pilot (default), the Pilot +visualizer's, or cameo, the look of Cameo Systems Modeler \(em a diagram frame +with a header tab, Arial text, gradient fills, compartments and the UML +pseudo\-state symbols \(em for a diagram migrated from Cameo to keep its look. +An interconnection or mixed rendering draws on each part the ports its +interconnection edges end at, each a small square on the part's border named +beside it; \-render\-ports full draws every port a part has, labelled name : +Type. A DiagramLayout Style on a member colours it over either look, and a +Note is drawn beside the member it is about. A view whose members carry +DiagramLayout positions draws the placed members and the edges between them in +every graph form, and leaves a member with no position undrawn: the DOT form +pins each at its stated place, so nothing lands on a positioned box, while +Mermaid, PlantUML and D2 lay the same members out themselves. +\-render\-unplaced strip draws the unplaced members too, in rows in a strip +below a DOT drawing. The same setting shapes the diagrams of +\-render\-document and \-render\-documents. +.SH LISTING DOCUMENTS AND VIEWS +.RS 2 +.nf +# the names \-render\-document reads +sysml model.sysml \-list documents +# the names \-render reads, with their kinds +sysml model.sysml \-list views +sysml model.sysml \-list diagrams \-list\-kind state,action +sysml model.sysml \-list all \-list\-form json +.fi +.RE +.PP +\-list documents lists the document definitions the loaded model declares, +\-list views its views with the rendering kind each states, a view whose kind +this build does not produce marked unsupported with the reason, \-list +diagrams the graph\-shaped views \(em tree, interconnection, state, action, +case, mixed, sequence, requirement, definition and package, the kinds a +document draws as a diagram \(em \-list pseudo\-views the pseudo\-views a +document declaring no view is rendered through, and \-list all the documents +followed by the views. Each name is the qualified name as the notation writes +it, every segment quoted that needs it and a line break escaped as \en, so it +is passed back unchanged to \-render\-document or \-render. +.PP +\-list\-kind keeps the views of the comma\-separated kinds it names. +\-list\-form text (default) writes one aligned line per item; tsv writes a +header record category, kind, supported, file, line, name and reason, then a +record per item; json writes an array of objects with those fields, each +omitted when empty. file is the path the declaration was loaded from and line +its 1\-based line. A model declaring nothing to list writes nothing. +.SH RENDERING A DOCUMENT +.RS 2 +.nf +# Markdown on stdout +sysml model.sysml \-render\-document Reports::MassReport +sysml model.sysml \-render\-document Reports::MassReport \-o report.md +sysml model.sysml \-render\-document Reports::MassReport \-doc\-form pdf \e + \-o report.pdf +sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e + \-o report.html +# every document, linked +sysml model.sysml \-render\-documents rendered +sysml model.sysml \-render\-documents site \-doc\-form html \-html\-css \e + theme.css +# a bundled theme +sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e + \-html\-theme report \-o report.html +# diagrams drawn in the browser +sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e + \-html\-mermaid cdn \-o report.html +# formulas typeset in the browser +sysml model.sysml \-render\-document Reports::MassReport \-doc\-form html \e + \-html\-math cdn \-o report.html +# diagrams as Graphviz DOT +sysml model.sysml \-render\-document Reports::MassReport \-diagram\-form \e + dot \-o report.md +# diagrams as PlantUML +sysml model.sysml \-render\-document Reports::MassReport \-diagram\-form \e + plantuml \-o report.md +# diagrams as D2 +sysml model.sysml \-render\-document Reports::MassReport \-diagram\-form \e + d2 \-o report.md +sysml model.sysml \-render\-document Reports::MassReport \-doc\-form pdf \e + \-pdf\-engine pandoc \-doc\-title\-page \-doc\-toc \-doc\-number\-sections \e + \-o report.pdf +# the default stylesheet +sysml \-html\-default\-css \-o sysml\-document.css +# a theme's full sheet +sysml \-html\-default\-css \-html\-theme modern \-o modern.css +.fi +.RE +.PP +A document is a part def specializing DocumentQueries::Document. Its queries +are bound in the model and run against it, and the result is written as +CommonMark\-compatible Markdown. Its diagram blocks are chosen per diagram: a +view some DiagramLayout::Layout or Route positions is drawn by Graphviz where +it states, as inline SVG when dot is installed and as a dot fence otherwise, +and every other graph\-shaped view is Mermaid source; with Graphviz absent a +positioned view falls back to Mermaid under a notice saying so. +\-diagram\-form mermaid, dot, plantuml or d2 writes every graph\-shaped one in +that form instead, in Markdown and HTML alike, while a table or matrix view +stays a table. D2 writes tree, interconnection, state, action, sequence, +requirement, definition and package renderings, and refuses case and mixed +diagrams. Neither Graphviz, PlantUML nor D2 is needed to write a fence. +.PP +\-doc\-form html writes semantic HTML instead, carrying each element's +identity and kind, styled by a stylesheet in a cascade layer your own CSS +overrides without !important. +.PP +\-doc\-form pdf lays that HTML out with a print stylesheet through an external +converter named by \-pdf\-engine \(em weasyprint (default) or prince, or +pandoc, which reads the Markdown instead \(em run as a subprocess, never +linked in; diagrams are pre\-rendered to SVG with mermaid\-cli (mmdc). +\-html\-theme, \-html\-css and \-html\-no\-default\-css style the PDF as they +style the page. None of these tools is needed until PDF output is asked for; +scripts/download\-doc\-pdf\-toolchain.sh provisions pinned copies. +.PP +HTML output needs nothing external and loads nothing by default: \-html\-theme +picks one of the bundled looks (default, acm, ieee, modern, nasa, print, +report; acm, ieee and nasa follow those bodies' manuscript conventions), +\-html\-css adds your own stylesheets, \-html\-no\-default\-css drops the +default one, \-html\-fragment writes the document element alone to embed in a +page of yours, and \-html\-default\-css writes the default sheet out \(em or, +with \-html\-theme, a theme's whole sheet \(em to start from. \-html\-mermaid +cdn has the page load a pinned Mermaid release from jsDelivr so a browser +draws the Mermaid diagrams, or names a URL of your own to load it from. +.PP +Mathematics is LaTeX: a Span styled math is an inline formula, a Formula block +a display one. Markdown writes them between $ and $$ delimiters, HTML in \e( +\e) and \e[ \e] inside sysml\-math elements, left as source until \-html\-math +cdn has the page load a pinned MathJax release from jsDelivr, or a URL of your +own; PDF typesets them with KaTeX (katex) as it pre\-renders diagrams with +mermaid\-cli. +.SH FLAG ORDER +Flags may be written before or after the model they apply to. A file named +like a flag is read as a file after \-\-, which ends the flags: sysml \-trace +\-\- \-m.sysml +.SH READING FROM STANDARD INPUT +.RS 2 +.nf +cat model.sysml | sysml \-validate \- # A lone \- names standard input +cat model.sysml | sysml \- \-convert ttl \-from sysml +.fi +.RE +.PP +What was read from standard input is called in diagnostics, and a file +really named "\-" is read by naming it ./\- instead. +.SH PROFILING A RUN +.RS 2 +.nf +# Report what the run cost, on stderr +sysml \-validate \-memstats model.sysml +# Write a heap profile for go tool pprof +sysml \-validate \-memprofile heap.out model.sysml +# Write a CPU profile for go tool pprof +sysml \-validate \-cpuprofile cpu.out model.sysml +.fi +.RE +.SH EXIT STATUS +Every run that is not a prompt exits: +.TP +.B 0 +It did what was asked. +.TP +.B 1 +The model answered false for a check. +.TP +.B 2 +What was asked could not be carried out at all \(em an unreadable file, a +model that did not analyse cleanly, an unresolved name, a failed conversion. +.TP +.B 3 +Part of what was asked was carried out: a \-render\-documents set in which +some document could not be rendered. The others were written, a page stating +the error stands in for each that was not, and each failure is reported with +the document's qualified name. +.SH OUTPUT STREAMS +What was asked for is reported on stdout and what went wrong on stderr, +prefixed "sysml: " unless it locates a finding in the source. +.SH ENVIRONMENT +.TP +.B OPENSYSML_LIBRARY_PATH +Directory to load the SysML/KerML standard library from, instead of the copy +embedded in the binary. +.TP +.B OPENSYSML_MAX_STEPS +Expression evaluations one run may spend before it is reported as a runaway. +Default 10000000. +.TP +.B OPENSYSML_MAX_ACTION_STEPS +Token\-flow steps one action run may perform. Default 1000000. +.TP +.B OPENSYSML_MAX_EVENTS +Events one state machine run may dispatch. Default 1000000. +.TP +.B OPENSYSML_MAX_DO_STEPS +Do actions one state machine run may perform. Default 5000000. +.TP +.B OPENSYSML_MAX_ELEMENTS +Collection elements one evaluation may hold, which bounds the memory a run +holds rather than the work it does. Default 1000000. +.TP +.B OPENSYSML_MAX_CALC_DEPTH +Nested calc invocations one run may hold on the stack, which is what a +recursion spends. Default 10000, ceiling 25000. +.TP +.B OPENSYSML_MAX_SWEEP_RUNS +Runs one parameter sweep or sample may make, each a whole analysis or calc run +of its own with its own budgets. Default 1000. +.TP +.B OPENSYSML_MAX_INTEGER_BITS +Bits the magnitude of one Integer a run computes may take; Integers are +otherwise unbounded, and a result past it, such as a large power, is refused +before it is computed. At least 64, so every machine\-word Integer fits; +default 1048576. +.TP +.B OPENSYSML_JOBS +Runs of one check that may go concurrently \(em the linearizations of an +exploration, the engines \-engine all consults \(em each on a worker of its +own over the shared model, and files of one load that are parsed and validated +at once; \-jobs and %jobs override it. Default one per CPU, fewer where the +memory available leaves less than 512 MiB per worker. +.TP +.B OPENSYSML_TOOLS +Directory of the tool manifest: one JSON file per external tool (toolName, +version, executable, variables), each registered as the engine tool: +that runs actions and calcs annotated ToolExecution with that toolName. Unset +registers no tool, and such an action or calc is refused. +.TP +.B OPENSYSML_TOOL_TIMEOUT +How long one tool invocation may take, as a Go duration. Default 10s, after +which the performance or calculation fails. +.TP +.B OPENSYSML_SMT +Executable the %check, %explain, %solve, %configure and %optimize commands +drive as their SMT solver, speaking SMT\-LIB2 on standard input +(experimental). Unset looks for z3, then cvc5, on PATH. +.TP +.B OPENSYSML_SMT_TIMEOUT +How long one solver query may take, as a Go duration. Default 10s, after which +the verdict is unknown; a check's \-check\-timeout takes its place for the smt +engine's queries. +.TP +.B OPENSYSML_SMT_CORE_BUDGET +How long %explain may spend reducing an unsat core to a minimal one, as a Go +duration. Default 30s. +.TP +.B OPENSYSML_SMT_MAX_CONFIGURATIONS +How many variant selections %configure ... all may report before saying the +enumeration was cut short. Default 32. +.PP +Each variable above also answers to its legacy SYSML_\-prefixed name +(SYSML_MAX_STEPS for OPENSYSML_MAX_STEPS, and so on), which remains accepted. +When both are set and the OPENSYSML_ value is non\-empty, the OPENSYSML_ value +wins; setting only the legacy name prints a one\-time deprecation warning on +standard error. +.PP +A budget bounds one run \(em one evaluation, one instantiation, one calc +invocation, one action, one state machine \(em not a whole session, so a long +session of small operations never exhausts one. A run started inside another +shares the outer run's budget. +.SH FILES +.TP +.B $XDG_STATE_HOME/sysml/history +Prompt history, when XDG_STATE_HOME is set and writable. +.TP +.B ~/.sysml_history +Prompt history otherwise. When neither can be written the history is kept for +the session only. +.SH REPORTING BUGS +Report bugs at https://github.com/Open\-MBEE/OpenSysML/issues. +.SH SEE ALSO +sysml\-lsp(1), sysml\-grpc(1), https://github.com/Open\-MBEE/OpenSysML