Skip to content
Merged
1 change: 1 addition & 0 deletions changes/unreleased/metamodel-table.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `sysml -metamodel-table` writes the pinned pilot metamodel's table as JSON — every metaclass with its parents and abstractness, every property with its defining class, kind, range, bounds, order, derivation, redefinitions, subsettings and opposite, every enumeration, and the pilot tag, commit and metamodel date — so generators outside this module (the QVTS reflection queries in Open-MBEE/qvts) follow the same table the API-JSON and RDF writers do. Like `-metaclass-graph`, it reads no model and is a run of its own.
13 changes: 13 additions & 0 deletions cmd/sysml/graphs.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,16 @@ func writeMetaclassGraph() error {
}
return writeOutputFile(outputPath, out, "metaclass hierarchy")
}

// writeMetamodelTable writes the generated metamodel table as JSON to stdout or to -output.
func writeMetamodelTable() error {
out, err := ontology.MarshalTable()
if err != nil {
return err
}
if outputPath == "" {
_, err := os.Stdout.Write(out)
return err
}
return writeOutputFile(outputPath, out, "metamodel table")
}
27 changes: 25 additions & 2 deletions cmd/sysml/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ var (
schedule schedulePolicy
listEngines bool
metaclassGraph bool
metamodelTable bool
explainCodes codeNames
probeEngines bool
engine engineSelection
Expand Down Expand Up @@ -499,7 +500,7 @@ func runCLI() int {
case len(args) > 0:
fmt.Fprintln(os.Stderr, "sysml: -html-default-css writes the default stylesheet, which no model shapes; ask for it without model files")
return 2
case modelRunRequested() || metaclassGraph:
case modelRunRequested() || metaclassGraph || metamodelTable:
fmt.Fprintln(os.Stderr, "sysml: -html-default-css writes the default stylesheet and nothing else; ask for it in its own run")
return 2
case docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering || docNumberFigures || htmlPageFlagsGiven():
Expand All @@ -523,7 +524,7 @@ func runCLI() int {
case len(args) > 0:
fmt.Fprintln(os.Stderr, "sysml: -metaclass-graph writes the metaclass hierarchy of the pinned metamodel, which no model shapes; ask for it without model files")
return 2
case modelRunRequested() || htmlShowCSS:
case modelRunRequested() || htmlShowCSS || metamodelTable:
fmt.Fprintln(os.Stderr, "sysml: -metaclass-graph writes the metaclass hierarchy and nothing else; ask for it in its own run")
return 2
case docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering || docNumberFigures || htmlPageFlagsGiven() ||
Expand All @@ -538,6 +539,28 @@ func runCLI() int {
return exitHolds
}

// The metamodel table is likewise a property of the pin: one JSON record of
// every metaclass, property and enumeration, for generators outside this module.
if metamodelTable {
switch {
case len(args) > 0:
fmt.Fprintln(os.Stderr, "sysml: -metamodel-table writes the pinned metamodel's table, which no model shapes; ask for it without model files")
return 2
case modelRunRequested() || htmlShowCSS:
fmt.Fprintln(os.Stderr, "sysml: -metamodel-table writes the metamodel table and nothing else; ask for it in its own run")
return 2
case docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering || docNumberFigures || htmlPageFlagsGiven() ||
fromFormat != "" || strictMode || syncBase != "" || syncState != "" ||
syncConfirmDeletes || syncMintIDs || syncAnnotate != "":
fmt.Fprintln(os.Stderr, "sysml: -metamodel-table writes the metamodel table, which reads no input; the input, repository and document options do not apply")
return 2
}
if err := writeMetamodelTable(); err != nil {
return fail(err)
}
return exitHolds
}

if renderDoc == "" && renderDocsDir == "" &&
(docForm != "" || diagramForm != "" || pdfEngine != "" || pdfTitlePage || pdfTOC || pdfNumbering || docNumberFigures || htmlFlagsGiven()) {
fmt.Fprintln(os.Stderr, "sysml: -doc-form, -diagram-form, the document options and the stylesheet options apply to -render-document and -render-documents; name the document to render")
Expand Down
61 changes: 61 additions & 0 deletions cmd/sysml/metamodel_table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/Open-MBEE/OpenSysML/internal/translate/rdf/ontology"
)

// TestMetamodelTableIsItsOwnRun pins that -metamodel-table writes the committed
// table as JSON, reads no model, stands in for no other mode and never writes
// over an input file.
func TestMetamodelTableIsItsOwnRun(t *testing.T) {
binary := buildCLI(t)

run := runCommand(t, exec.Command(binary, "-metamodel-table"))
wantReport(t, run, 0, `"pilotTag"`, `"definingClass": "Element"`, `"name": "PartUsage"`)
var table ontology.Table
if err := json.Unmarshal([]byte(run.stdout), &table); err != nil {
t.Fatalf("the output is not the table's JSON: %v", err)
}
if table.Metamodel != ontology.Version || table.PilotTag != ontology.SourceTag || table.PilotCommit != ontology.SourceCommit {
t.Errorf("header = %s %s %s, want the table's", table.Metamodel, table.PilotTag, table.PilotCommit)
}
if len(table.Classes) != len(ontology.Classes()) || len(table.Properties) != len(ontology.Properties()) ||
len(table.Enumerations) != len(ontology.Enumerations()) {
t.Errorf("counts = %d classes, %d properties, %d enumerations; want the table's",
len(table.Classes), len(table.Properties), len(table.Enumerations))
}

dir := t.TempDir()
model := filepath.Join(dir, "m.sysml")
const source = "package P;\n"
if err := os.WriteFile(model, []byte(source), 0o644); err != nil {
t.Fatal(err)
}
wantReport(t, runCommand(t, exec.Command(binary, model, "-metamodel-table")),
2, "ask for it without model files")
wantReport(t, runCommand(t, exec.Command(binary, "-metamodel-table", "-convert", "ttl")),
2, "ask for it in its own run")
wantReport(t, runCommand(t, exec.Command(binary, "-metamodel-table", "-metaclass-graph")),
2, "ask for it in its own run")
wantReport(t, runCommand(t, exec.Command(binary, "-metamodel-table", "-from", "kerml")),
2, "the input, repository and document options do not apply")
if got, err := os.ReadFile(model); err != nil || string(got) != source {
t.Fatalf("the model file changed: %q, %v", got, err)
}

out := filepath.Join(dir, "metamodel.json")
wantReport(t, runCommand(t, exec.Command(binary, "-metamodel-table", "-o", out)), 0)
written, err := os.ReadFile(out)
if err != nil {
t.Fatal(err)
}
if string(written) != run.stdout {
t.Errorf("-o writes the bytes that stdout carries; they differ")
}
}
3 changes: 3 additions & 0 deletions cmd/sysml/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func doc() usage.Doc {
usage.Ex("sysml -engines", "List engines, kind and status; nothing runs"),
usage.Ex("sysml -engines -probe", "Also start each external engine once"),
usage.Ex("sysml -metaclass-graph -o metaclasses.ttl", "Write the metaclass hierarchy as Turtle"),
usage.Ex("sysml -metamodel-table -o metamodel.json", "Write the metamodel table as JSON"),
usage.Ex("sysml -engine explore -action Drive m.sysml", "The same as -schedule explore"),
usage.Ex("sysml -engine all -requirement R m.sysml", "Every engine that covers the question"),
usage.Ex("sysml -engine run -json -constraint C m.sysml", "One engine; the plan in the report"),
Expand Down Expand Up @@ -697,6 +698,7 @@ func registerFlags(fs *flag.FlagSet) {
fs.Var(&modelChecks.samples, "samples", "Draw this many values uniformly from each -sweep range instead of stepping through it; needs -seed")
fs.Var(&jobsFlag, "jobs", "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")

fs.BoolVar(&metamodelTable, "metamodel-table", false, "Write the pinned pilot metamodel's table as JSON — every metaclass with its parents, every property with its defining class, kind, range, bounds and flags, every enumeration, and the pilot tag, commit and metamodel date — to stdout or -o, without a model, and exit")
fs.BoolVar(&metaclassGraph, "metaclass-graph", false, "Write the SysML metaclass hierarchy of the pinned pilot metamodel as Turtle (rdfs:Class, rdfs:subClassOf, sysx:abstract) to stdout or -o, without a model, and exit")
fs.BoolVar(&listEngines, "engines", false, "List the analysis engines this build knows and their status, spawning nothing, and exit")
fs.BoolVar(&probeEngines, "probe", false, "With -engines, also start each external engine once and report the outcome as its status")
Expand Down Expand Up @@ -848,6 +850,7 @@ func optionGroups() []usage.OptionGroup {
usage.Opt("convert", formatArg),
usage.Opt("stdlib", ""),
usage.Opt("metaclass-graph", ""),
usage.Opt("metamodel-table", ""),
usage.Opt("migrate", formatArg),
usage.Opt("from", formatArg),
usage.Opt("import", fileArg),
Expand Down
1 change: 1 addition & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ written in, so the verdicts are about that object:
| `-check-timeout <duration>` | With `-engine check`, `-engine smt` or `-engine all`: the wall clock the check's plan may run for, as `30s` or `2m`, and the time each of the `smt` engine's solver queries may take, in place of `OPENSYSML_SMT_TIMEOUT`; a search the clock stops is reported `incomplete: time` with the states and depth it reached, not as a verdict, and exits 2. Unbounded by default, the solver's queries at `OPENSYSML_SMT_TIMEOUT` |
| `-engines` | Lists the analysis engines this build knows — name, kind, protocol, authority, the question kinds each answers and its status — and exits, without a model and without starting a process: the external engines of `OPENSYSML_ENGINES` and the tools of `OPENSYSML_TOOLS` are listed from their manifests alone, each followed by a line naming its file and command. See [Analysis engines](#analysis-engines) |
| `-metaclass-graph` | Writes the SysML metaclass hierarchy of the pinned pilot metamodel as Turtle to stdout, or to `--output`, and exits without reading a model: one `rdfs:Class` per metaclass in the `sysml:` namespace, `rdfs:subClassOf` per declared general metaclass and `sysx:abstract true` on the abstract ones, the file committed as `internal/translate/rdf/ontology/sysml-metaclasses.ttl`. A store holding one `rdf:type` per element finds the specializations of a metaclass through `rdf:type/rdfs:subClassOf*` once it is loaded. See [Metaclass hierarchy graph](rdf-mapping.md#metaclass-hierarchy-graph) |
| `-metamodel-table` | Writes the pinned pilot metamodel's table as JSON to stdout, or to `--output`, and exits without reading a model: `metamodel` (the ecore namespace date), `pilotTag`, `pilotCommit`, then `classes` (name, `abstract`, `parents`), `properties` (name, `definingClass`, `kind` object or datatype, `range` as a metaclass or enumeration name or a datatype IRI, `lower`, `upper` as ecore states it, -1 unbounded and -2 unspecified, `ordered`, `nonunique`, `derived`, `default`, `redefines`, `subsets`, `opposite`) and `enumerations` (name, `literals`) — the same table (`internal/translate/rdf/ontology/table.go`, generated from the pilot's `SysML.ecore`) the API-JSON and RDF writers follow, for generators that live outside this module. See [Metaclass hierarchy graph](rdf-mapping.md#metaclass-hierarchy-graph) |
| `-probe` | With `-engines`, also start each external engine once, check its `describe` against its manifest entry field by field and report the outcome as its status (`ready (…; describe agrees)`, or the first field that disagrees). See [External engines](external-engines.md) |
| `-engine <name>\|auto\|all` | The analysis engine every check of the invocation is put to. `auto` (the default) picks the engine of highest authority covering the question and advances past one that refuses or answers *not covered*, reaching an external engine only after every built-in one has; a name (`run`, `explore`, `check`, `smt`, `sweep`, `solve`, or an external engine's) puts the question to that engine alone, and its refusal is the answer; `all` puts it to every engine covering it, one after another in name order, and composes their answers. A name no engine is registered under is refused before anything runs. `-engine explore` explores as `-schedule explore` does; `-engine check` searches every schedule of each `-action` for a violation, a deadlock, a failure or a divergence ([Checking every schedule of an action](#checking-every-schedule-of-an-action-or-a-state-machine)); `-engine smt` decides a `-check-property` over every schedule and every value of the free inputs with an SMT solver ([Deciding a property over the inputs](#deciding-a-property-over-the-inputs)). See [Analysis engines](#analysis-engines) |
| `-jobs <n>` | Runs of one check that may go concurrently — the linearizations of an exploration, the rows of a `-sweep`/`-samples`, the engines `-engine all` consults — each on a worker of its own over the shared model, and how many files of one load are parsed and validated at once (`-validate`, `-satisfy`, `-check` and every mode that loads files). `n` is a positive integer; the default is `OPENSYSML_JOBS`, else one per CPU, fewer where the memory available leaves less than 512 MiB per worker (Linux: `MemAvailable` and the cgroup's `memory.max`; one at least). The result of a check is the same at any count: the outcome table, the witness, the run count and the cut a violation makes are those of the runs taken one at a time in plan order. See [Running in parallel](#running-in-parallel) |
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/rdf-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,7 @@ as an RDF vocabulary graph a store loads beside its models:
```bash
sysml -metaclass-graph # Turtle to stdout; no model is read
sysml -metaclass-graph -o sysml-metaclasses.ttl
sysml -metamodel-table -o metamodel.json # the whole table as JSON: classes, properties, enumerations
```

The graph is written from the metamodel table of
Expand Down Expand Up @@ -2381,7 +2382,7 @@ exactly, which a converted model never contains.
| `internal/translate/convert` | The `Convert` entry point: format names, notation parsing, the SysML v1 migration |
| `tests/corpus/roundtrip_test.go` | The per-file round-trip ratchets over every model under `examples/`, with their baselines in `testdata/corpus_roundtrip_expected.txt` (Turtle) and `testdata/api_json_roundtrip_expected.txt` (the API element form) ([rdf-corpus-roundtrip.md](../project/rdf-corpus-roundtrip.md)) |
| `internal/frontend/repl` | `%save` |
| `cmd/sysml` | `-convert`, `-from`, `-o`, `-metaclass-graph` |
| `cmd/sysml` | `-convert`, `-from`, `-o`, `-metaclass-graph`, `-metamodel-table` |

The RDF layer is hand-written against the Turtle grammar rather than pulled in
as a dependency: the subset needed here is small, and the parser rejects what it
Expand Down
1 change: 1 addition & 0 deletions internal/frontend/mcp/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const (
// adding the field or RPC and the tool moves the flag into an entry.
var Unserved = map[string]string{
"metaclass-graph": "no RPC answers the metaclass hierarchy vocabulary graph",
"metamodel-table": "no RPC answers the pinned metamodel's table as JSON",
"portable": "MigrateRequest has no portable field",
"render-all": "no RPC renders every declared view",
"render-form": notDrawn,
Expand Down
11 changes: 11 additions & 0 deletions internal/translate/rdf/ontology/ontology.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ type Property struct {
// Many reports an upper multiplicity above 1 or unbounded (ecore upperBound
// -1): the API JSON shape is an array.
Many bool
// Upper is the ecore upperBound as stated: -1 unbounded, -2 unspecified, else the
// bound; the default 1 is left zero in the table, which UpperBound reads as 1.
Upper int
// Ordered reports a Many property whose values are ordered (ecore
// ordered, which defaults to true); a single-valued property is never Ordered.
Ordered bool
Expand All @@ -70,6 +73,14 @@ type Property struct {
Opposite string
}

// UpperBound is the upper multiplicity as ecore spells it: -1 unbounded, -2 unspecified, else the bound.
func (p Property) UpperBound() int {
if p.Upper == 0 {
return 1
}
return p.Upper
}

// QualifiedName returns the property as "DefiningClass::name", the form
// Redefines, Subsets and Opposite name properties in.
func (p Property) QualifiedName() string { return p.DefiningClass + "::" + p.Name }
Expand Down
Loading
Loading