@openmbee/opensysml 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +355 -33
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.js +16 -0
- package/dist/browser/wasm-worker.d.ts +1 -0
- package/dist/browser/wasm-worker.js +2 -0
- package/dist/browser/wasm.d.ts +17 -0
- package/dist/browser/wasm.js +94 -0
- package/dist/core/arguments.d.ts +4 -0
- package/dist/core/arguments.js +19 -0
- package/dist/core/capabilities.d.ts +90 -9
- package/dist/core/capabilities.js +111 -10
- package/dist/core/connection.d.ts +203 -2
- package/dist/core/connection.js +810 -17
- package/dist/core/conversion.d.ts +153 -0
- package/dist/core/conversion.js +191 -0
- package/dist/core/document.d.ts +188 -0
- package/dist/core/document.js +457 -0
- package/dist/core/edit.d.ts +446 -0
- package/dist/core/edit.js +1994 -0
- package/dist/core/engines.d.ts +64 -0
- package/dist/core/engines.js +74 -0
- package/dist/core/errors.d.ts +164 -2
- package/dist/core/errors.js +209 -3
- package/dist/core/exploration.d.ts +71 -0
- package/dist/core/exploration.js +116 -0
- package/dist/core/graphs.d.ts +15 -0
- package/dist/core/graphs.js +3 -0
- package/dist/core/index.d.ts +30 -9
- package/dist/core/index.js +14 -5
- package/dist/core/model.d.ts +168 -8
- package/dist/core/model.js +258 -31
- package/dist/core/package.d.ts +2 -0
- package/dist/core/package.js +2 -0
- package/dist/core/query.d.ts +50 -0
- package/dist/core/query.js +197 -0
- package/dist/core/render-view.d.ts +97 -0
- package/dist/core/render-view.js +120 -0
- package/dist/core/sources.d.ts +34 -0
- package/dist/core/sources.js +134 -0
- package/dist/core/status.d.ts +9 -2
- package/dist/core/status.js +24 -8
- package/dist/core/typed.d.ts +71 -0
- package/dist/core/typed.js +245 -0
- package/dist/core/values.d.ts +127 -8
- package/dist/core/values.js +536 -30
- package/dist/core/verdict.d.ts +271 -0
- package/dist/core/verdict.js +567 -0
- package/dist/core/wasm.d.ts +114 -0
- package/dist/core/wasm.js +514 -0
- package/dist/generated/sysml_pb.d.ts +1080 -22
- package/dist/generated/sysml_pb.js +200 -100
- package/dist/node/binary.d.ts +2 -0
- package/dist/node/binary.js +8 -6
- package/dist/node/generate-cli.d.ts +2 -0
- package/dist/node/generate-cli.js +10 -0
- package/dist/node/generate.d.ts +99 -0
- package/dist/node/generate.js +757 -0
- package/dist/node/index.d.ts +29 -2
- package/dist/node/index.js +211 -10
- package/dist/node/service.d.ts +5 -3
- package/dist/node/service.js +32 -26
- package/dist/node/wasm-source.d.ts +18 -0
- package/dist/node/wasm-source.js +103 -0
- package/dist/node/wasm-worker.d.ts +1 -0
- package/dist/node/wasm-worker.js +10 -0
- package/dist/node/wasm.d.ts +17 -0
- package/dist/node/wasm.js +63 -0
- package/package.json +19 -7
- package/release-digests.json +105 -0
package/README.md
CHANGED
|
@@ -5,10 +5,18 @@ models over the `sysml-grpc` service, using the [Connect
|
|
|
5
5
|
protocol](https://connectrpc.com/docs/protocol) with protobuf bodies. No native
|
|
6
6
|
addon, so an install is a plain registry fetch.
|
|
7
7
|
|
|
8
|
+
For a task-oriented walkthrough, see the
|
|
9
|
+
[Node client guide](https://runtime.opensysml.org/clients/node/).
|
|
10
|
+
|
|
8
11
|
```bash
|
|
9
|
-
npm install @openmbee/opensysml
|
|
12
|
+
npm install @openmbee/opensysml
|
|
10
13
|
```
|
|
11
14
|
|
|
15
|
+
A development snapshot is published every night under the `nightly` dist-tag
|
|
16
|
+
(`npm install @openmbee/opensysml@nightly`), with its platform packages carrying
|
|
17
|
+
that night's `sysml-grpc`; `latest` stays the stable release. See [Nightly
|
|
18
|
+
snapshots](https://runtime.opensysml.org/project/nightly/).
|
|
19
|
+
|
|
12
20
|
```ts
|
|
13
21
|
import { loads, connect } from "@openmbee/opensysml";
|
|
14
22
|
|
|
@@ -82,8 +90,8 @@ third a field the answer did not carry.
|
|
|
82
90
|
`SysMLVerdict` (`holds` / `fails` / `undecided`) and `FeatureValue` (`single` /
|
|
83
91
|
`many` / `error`) are unions of the same shape; every verdict arm carries a `standing` — the
|
|
84
92
|
engine that answered, the strength of its evidence (`observed`, `witnessed`, `bounded`,
|
|
85
|
-
`proved`) and the bounds it ran under
|
|
86
|
-
capability. Integers are `bigint`, because
|
|
93
|
+
`proved`) and the bounds it ran under, with `reported` and `reached` beside them —
|
|
94
|
+
empty from a service without the `engines` capability. Integers are `bigint`, because
|
|
87
95
|
the service's `int64` does not fit a `number` — an exact comparison against a
|
|
88
96
|
scenario expectation would otherwise be a lie.
|
|
89
97
|
|
|
@@ -168,6 +176,91 @@ Two limits to plan for rather than discover:
|
|
|
168
176
|
`fetch` transport, and asserts the allowed origin is answered on the preflight
|
|
169
177
|
while another origin is not.
|
|
170
178
|
|
|
179
|
+
## WebAssembly, without a service
|
|
180
|
+
|
|
181
|
+
`connectWasm()` uses the same `Connection`, `Model`, values and errors over the
|
|
182
|
+
combined `sysml-wasm` module. It serves `ParseFile`, `ParseSources`,
|
|
183
|
+
`GetDiagnostics`, `GetSymbol`, `Evaluate`, `Instantiate`, `ExecuteAction`,
|
|
184
|
+
`ExecuteState` and `GetServerInfo`. Those are the module's complete RPC surface;
|
|
185
|
+
other capability-gated operations fail with `MissingCapabilityError`, and a
|
|
186
|
+
direct unsupported RPC fails with `UNIMPLEMENTED`.
|
|
187
|
+
The adapter uses JSON encoding; requesting protobuf encoding is refused.
|
|
188
|
+
|
|
189
|
+
In Node, install the optional `@openmbee/opensysml-wasm` package at the same
|
|
190
|
+
version as this client. `connectWasm()` resolves its module and matching Go
|
|
191
|
+
runtime automatically and runs a worker thread by default:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm install @openmbee/opensysml@<version> @openmbee/opensysml-wasm@<version>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { connectWasm } from "@openmbee/opensysml";
|
|
199
|
+
|
|
200
|
+
await using connection = await connectWasm();
|
|
201
|
+
const model = await connection.loads("package Demo { part def Car; }");
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
To use a module from another source, pass both the module and its matching Go
|
|
205
|
+
runtime:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
await using connection = await connectWasm({
|
|
209
|
+
wasm: "./sysml-wasm.wasm",
|
|
210
|
+
wasmExec: "/path/to/the/matching/wasm_exec.js",
|
|
211
|
+
});
|
|
212
|
+
const model = await connection.loads("package Demo { part def Car; }");
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The WASM module and `wasm_exec.js` must come from compatible Go toolchains.
|
|
216
|
+
The worker remains referenced while the connection is open, so `close()` or
|
|
217
|
+
`await using` ends it. `thread: "inline"` runs Go on the calling thread instead;
|
|
218
|
+
it is useful when a worker is unavailable, but blocks that thread during a call.
|
|
219
|
+
Closing an inline connection disables its client surface; Go has no exit hook to
|
|
220
|
+
stop the running module.
|
|
221
|
+
A worker-mode deadline rejects the waiting call without interrupting Go, so
|
|
222
|
+
later worker calls queue behind work that outlived its deadline. Inline calls
|
|
223
|
+
run synchronously and cannot be interrupted while they block the JavaScript
|
|
224
|
+
thread.
|
|
225
|
+
|
|
226
|
+
In a browser, omit `worker` to run inline, or provide a module worker serving the
|
|
227
|
+
package's `browser/wasm-worker` entry point:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { connectWasm } from "@openmbee/opensysml/browser";
|
|
231
|
+
|
|
232
|
+
const worker = new Worker("/assets/opensysml-wasm-worker.js", { type: "module" });
|
|
233
|
+
await using connection = await connectWasm({
|
|
234
|
+
wasm: new URL("./sysml-wasm.wasm", import.meta.url),
|
|
235
|
+
wasmExec: new URL("./wasm_exec.js", import.meta.url),
|
|
236
|
+
worker,
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
If the page loads `wasm_exec.js` itself, omit `wasmExec` to use the installed Go
|
|
241
|
+
constructor.
|
|
242
|
+
|
|
243
|
+
The browser worker module can be bundled from
|
|
244
|
+
`@openmbee/opensysml/browser/wasm-worker`. Bundle the module and runtime from
|
|
245
|
+
the npm package with:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const wasm = new URL("@openmbee/opensysml-wasm/sysml-wasm.wasm", import.meta.url);
|
|
249
|
+
const wasmExec = new URL("@openmbee/opensysml-wasm/wasm_exec.js", import.meta.url);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Or fetch both from jsDelivr, replacing `<version>` with the matching package
|
|
253
|
+
version:
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
https://cdn.jsdelivr.net/npm/@openmbee/opensysml-wasm@<version>/sysml-wasm.wasm
|
|
257
|
+
https://cdn.jsdelivr.net/npm/@openmbee/opensysml-wasm@<version>/wasm_exec.js
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Browser callers pass those URLs as `wasm` and `wasmExec`; browser package
|
|
261
|
+
resolution is not automatic. The combined module measures about 7.8 MB gzipped
|
|
262
|
+
and 5.5 MB with Brotli.
|
|
263
|
+
|
|
171
264
|
## Protobuf, not JSON
|
|
172
265
|
|
|
173
266
|
Bodies are protobuf by default. JSON is available (`connect({ encoding: "json" })`)
|
|
@@ -228,7 +321,8 @@ Every failure is an `OpenSysMLError`. A call the service refused is a
|
|
|
228
321
|
`"INVALID_ARGUMENT"`, …), and the statuses worth catching by themselves have a
|
|
229
322
|
subclass: `ModelNotFoundError` (the service no longer holds that hash),
|
|
230
323
|
`ModelFileNotFoundError`, `InvalidRequestError`, `ServiceTimeoutError`,
|
|
231
|
-
`
|
|
324
|
+
`ServiceUnavailableError` (the service was unreachable or died before
|
|
325
|
+
answering), `UnsupportedOperationError`. A name the model has not got is a
|
|
232
326
|
`SymbolNotFoundError`, which carries the `symbolName` it looked for and the
|
|
233
327
|
`suggestions` closest to it:
|
|
234
328
|
|
|
@@ -242,6 +336,22 @@ try {
|
|
|
242
336
|
}
|
|
243
337
|
```
|
|
244
338
|
|
|
339
|
+
The wider surface adds its own:
|
|
340
|
+
|
|
341
|
+
| error | what happened |
|
|
342
|
+
| --- | --- |
|
|
343
|
+
| `StaleServiceError` | the running service reports another version than `version` asked for |
|
|
344
|
+
| `ExecutionError` | an execution the service ran failed; carries `diagnostics` |
|
|
345
|
+
| `WrongKindError` | a verification or analysis named a symbol of another kind |
|
|
346
|
+
| `AnalysisRunError` | an analysis run failed before it could report |
|
|
347
|
+
| `ConversionError` | the service could not write the notation asked for |
|
|
348
|
+
| `MigrationError` | the service could not read the SysML v1 model, so nothing of it was migrated |
|
|
349
|
+
| `UnsupportedValueError` | the service sent a value this version of the client cannot decode |
|
|
350
|
+
| `QueryError` | a `Query` failed in-band |
|
|
351
|
+
| `DocumentQueryError` | a `runDocumentQuery` failed in-band |
|
|
352
|
+
| `EditError` | an edit was refused — `failure` names which, and the subclasses (`NoEditsError`, `EditTargetError`, `InvalidEditError`, `IllegalMemberKindError`, `RenameReferencedError`, `OverlappingEditsError`, `EditResultError`, `OwnerNotFoundError`, `OwnerNotNamespaceError`, `MemberNameTakenError`, `DeleteReferencedError`, `OwnerInsideTargetError`, `MoveReferencedError`, `ReferencedElsewhereError`) catch one kind of refusal |
|
|
353
|
+
| `TypeMismatchError` / `InstanceTypeError` / `FeatureValueError` | a typed view read a feature of another kind, an instance of another type, or a slot that is an error |
|
|
354
|
+
|
|
245
355
|
Source that does not parse is not a failure: `load`/`loads` return a model whose
|
|
246
356
|
`hasErrors` is true and whose `diagnostics` say where. Each `ModelDiagnostic` has
|
|
247
357
|
`severity`, `message`, `code` and an optional location; branch on `code`
|
|
@@ -310,8 +420,10 @@ In order, and each step refuses rather than falling back to the next:
|
|
|
310
420
|
1. **A shipped pin.** `release-digests.json`, synced from
|
|
311
421
|
`client/release-digests.json` by `python3 scripts/sync-release-digests.py`
|
|
312
422
|
and published in the tarball, pins the SHA-256 of every asset of a release.
|
|
313
|
-
|
|
314
|
-
|
|
423
|
+
The release job stamps the release it publishes into the tarball's copy
|
|
424
|
+
before packing it, so a published package pins its own release. Where the
|
|
425
|
+
table pins one, that is what the bytes must hash to, and a served `.sha256`
|
|
426
|
+
that disagrees is tampering: the download fails.
|
|
315
427
|
2. **The release's signed manifest.** With no pin, the client downloads
|
|
316
428
|
`SHA256SUMS.txt` and its sigstore bundle `SHA256SUMS.txt.bundle`, verifies
|
|
317
429
|
the bundle against the release pipeline's certificate identity (the CircleCI
|
|
@@ -361,21 +473,224 @@ install @openmbee/opensysml` must not need `buf`, Go or a network fetch of a
|
|
|
361
473
|
plugin, and a published tarball has to contain the compiled output. CI runs
|
|
362
474
|
`make proto-ts` and fails on any diff, so committed and generated cannot drift.
|
|
363
475
|
|
|
364
|
-
##
|
|
476
|
+
## Several documents
|
|
477
|
+
|
|
478
|
+
`connection.parseSources` parses several documents as one model — files the
|
|
479
|
+
service reads, or inline content named for its diagnostics:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { SourceDocument } from "@openmbee/opensysml";
|
|
483
|
+
|
|
484
|
+
const model = await connection.parseSources([
|
|
485
|
+
SourceDocument.file("library.sysml"),
|
|
486
|
+
SourceDocument.inline("user.sysml", "package User { import Library::*; }"),
|
|
487
|
+
]);
|
|
488
|
+
model.documents; // the names the parse gave them, in order
|
|
489
|
+
model.roots; // one root symbol per document
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Conversion and save
|
|
493
|
+
|
|
494
|
+
`connection.convert` writes a model, a file or source out in another notation —
|
|
495
|
+
`"sysml"`, `"kerml"`, `"turtle"`, `"api-json"` — and `save` writes the result to
|
|
496
|
+
a path, converting when needed:
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
import { save } from "@openmbee/opensysml";
|
|
500
|
+
|
|
501
|
+
const rdf = await connection.convert("turtle", { modelHash: model.hash });
|
|
502
|
+
await save(model, "out.ttl"); // format comes from the extension
|
|
503
|
+
await save(rdf, "copy.ttl"); // a Conversion writes what it holds
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
An experimental conversion emits a warning (`process.emitWarning`) rather than
|
|
507
|
+
failing, and reports it through `Conversion.experimentalNotice`.
|
|
508
|
+
|
|
509
|
+
## Migrating a SysML v1 model
|
|
365
510
|
|
|
366
|
-
|
|
367
|
-
`
|
|
368
|
-
|
|
511
|
+
A SysML v1 model — a Cameo/MagicDraw `.mdzip`, a UML XMI `.xmi` or an Eclipse
|
|
512
|
+
UML2 `.uml` export — is **migrated, not converted**: a conversion is lossless,
|
|
513
|
+
and a migration accounts for every v1 element as `mapped`, `approximated`,
|
|
514
|
+
`unmapped` or `skipped`. `connection.convert` refuses one with an
|
|
515
|
+
`InvalidRequestError` that says so and names `migrate`, whether `fromFormat` is
|
|
516
|
+
`xmi`, `uml` or `mdzip` or the path's extension is; `connection.migrate` is the
|
|
517
|
+
verb, and `save` writes what it answers with its image files beside it:
|
|
369
518
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
`
|
|
375
|
-
|
|
519
|
+
```ts
|
|
520
|
+
const migration = await connection.migrate("sysml", { path: "Model.mdzip" }, { report: true });
|
|
521
|
+
console.log(migration.report.summary); // migrated 93 element(s): 78 mapped, 12 approximated, …
|
|
522
|
+
for (const entry of migration.report.byVerdict("unmapped")) {
|
|
523
|
+
console.log(`${entry.kind} ${entry.name}: ${entry.note}`);
|
|
524
|
+
}
|
|
525
|
+
await save(migration, "Model.sysml"); // and images/… beside it
|
|
526
|
+
```
|
|
376
527
|
|
|
377
|
-
`
|
|
378
|
-
|
|
528
|
+
`save` refuses, with a `RangeError` and before writing anything, a path that is
|
|
529
|
+
the v1 model itself and an image that would land outside the model's directory
|
|
530
|
+
or over the model, as `sysml -migrate -o` does.
|
|
531
|
+
|
|
532
|
+
Inline `content` is the file's bytes (`Uint8Array`) and needs `fromFormat` to
|
|
533
|
+
say which form they are; a v2 `fromFormat` is refused with a pointer at
|
|
534
|
+
`convert`. The `Migration` carries the notation (or Turtle, `"ttl"`) and a
|
|
535
|
+
`MigrationReport` whose `summary` and four counts always come back; `report:
|
|
536
|
+
true` adds every element's `MigrationEntry` and the `text` the command's
|
|
537
|
+
`-migration-report` writes, `results: true` the `-migration-results` index,
|
|
538
|
+
and `layoutPath`/`layoutContent`, `imageBaseUrl` and `strict` are the other
|
|
539
|
+
companion flags. Migration is experimental and warns as the RDF direction does;
|
|
540
|
+
a model the service cannot read at all is a `MigrationError`.
|
|
541
|
+
|
|
542
|
+
## Query and documents
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
const elements = await model.query({ oslc: "sysml:name = \"Wheel\"" });
|
|
546
|
+
const rows = await model.runDocumentQuery("Observatory::PartsList", {
|
|
547
|
+
subject: new ElementRef("Demo::sedan"),
|
|
548
|
+
});
|
|
549
|
+
const markdown = await model.renderDocument("Observatory::Overview");
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
`query` also takes a structured form (`payload`/`scope`/`select`/`where`, or a
|
|
553
|
+
wire `query`), never both a structured form and `oslc`. A binding value is an
|
|
554
|
+
`ElementRef`, an `ObjectRef`, a primitive or a quantity; `runDocumentQuery`
|
|
555
|
+
answers the typed `DocumentQueryResult` — `rows` of `DocumentValue`s, the schema
|
|
556
|
+
the query declared — and `renderDocument` answers Markdown, or HTML with
|
|
557
|
+
`{ form: "html" }`.
|
|
558
|
+
|
|
559
|
+
## Execution and exploration
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
const result = await model.executeAction("Demo::Drive", {
|
|
563
|
+
inputs: { distance: 120 },
|
|
564
|
+
});
|
|
565
|
+
const state = await model.executeState("Demo::Ignition");
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
`executeAction` and `executeState` answer the run — `outputs`, `events`, the
|
|
569
|
+
`trace` of each step — and refuse `schedule: "explore"`, because an exploration
|
|
570
|
+
is not a run: `exploreAction`, `exploreState` and `exploreAnalysis` answer an
|
|
571
|
+
`Exploration` (`outcomes`, `runs`, `budgetsHit`) instead. `{ schedule: "explore" }`
|
|
572
|
+
is their default; any other schedule there is refused the same way. A
|
|
573
|
+
`performer` names the engine a run is handed to, where the service schedules.
|
|
574
|
+
|
|
575
|
+
## Verification, analysis and sweep
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
const verdict = await model.verifyConstraint("Demo::MassBudget", {
|
|
579
|
+
subject: "Demo::sedan",
|
|
580
|
+
question: "worst-case",
|
|
581
|
+
});
|
|
582
|
+
verdict.verdict.kind; // "holds" | "fails" | "undecided"
|
|
583
|
+
await model.satisfied(); // every asserted satisfy holds
|
|
584
|
+
|
|
585
|
+
const validation = await model.validateInstance("Demo::sedan");
|
|
586
|
+
const computed = await model.calc("Demo::TotalMass", { arguments: [2] });
|
|
587
|
+
const analysis = await model.runAnalysis("Demo::TradeStudy", { subject: "Demo::sedan" });
|
|
588
|
+
const table = await model.runSweep("Demo::TradeStudy", {
|
|
589
|
+
power: [{ kind: "int", value: 100n }, { kind: "int", value: 300n }, { kind: "int", value: 50n }],
|
|
590
|
+
}, { samples: 25, seed: 7 });
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
A verification raises `WrongKindError` for a symbol of another kind and keeps a
|
|
594
|
+
`fails` or `undecided` verdict as an answer rather than a failure.
|
|
595
|
+
`runAnalysis` answers the run's `outputs`, `verdicts`, `instances` and
|
|
596
|
+
`diagnostics` — a failed analysis still answers what it completed.
|
|
597
|
+
`runSweep` takes a `Record` of `parameter → [from, to]` or `[from, to, step]`
|
|
598
|
+
ranges and answers the `SweepTable` of rows, each carrying its own error when a
|
|
599
|
+
run fails.
|
|
600
|
+
|
|
601
|
+
## Engines
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
for (const engine of await connection.listEngines()) {
|
|
605
|
+
engine.name; engine.authority; // what it answers: verification, analysis, ...
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
`engine` selects one on the calls above (`verifyConstraint`, `validateInstance`,
|
|
610
|
+
`calc`, `runAnalysis`, `verifySatisfaction`), and `explainVerdict` /
|
|
611
|
+
`explainStanding` say how a verdict was reached — the engine and the strength of
|
|
612
|
+
its evidence (`observed`, `witnessed`, `bounded`, `proved`).
|
|
613
|
+
|
|
614
|
+
## Authoring
|
|
615
|
+
|
|
616
|
+
`model.edit()` starts a batch of source-preserving edits, applied atomically:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
const editor = model.edit();
|
|
620
|
+
editor.addItemDef("Car", "Demo");
|
|
621
|
+
editor.rename("Demo::Wheel", "Tire");
|
|
622
|
+
const applied = await editor.apply();
|
|
623
|
+
applied.documents; // every document the batch rewrote, by parse name
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
Every `add*` of the Python client is here — `addPackage` … `addRequirement`,
|
|
627
|
+
`addCalcDef`, `addParameter`, `addPerformAction`, `addStateAction`,
|
|
628
|
+
`addAssertConstraint`, `addTransition`, `addImport`, `addDocumentation`,
|
|
629
|
+
`addMetadata`, `addVerify`, `addObjective`, `addSatisfy`, plus `setValue`,
|
|
630
|
+
`rename`, `delete(target, { cascade })` and `move(target, owner)` — and
|
|
631
|
+
`Body` builds guarded then/else and nested if/while/for blocks for action
|
|
632
|
+
bodies. Edits are validated eagerly (a `RangeError` or `TypeError` at the
|
|
633
|
+
`add*` call, not at `apply()`) and capability-gated per operation and per
|
|
634
|
+
field. `connection.applyEdits(modelHash, operations)` is the low-level form,
|
|
635
|
+
taking wire `EditOperation` messages.
|
|
636
|
+
|
|
637
|
+
An edit the service refuses raises the typed error for the failure —
|
|
638
|
+
`EditTargetError`, `RenameReferencedError`, `DeleteReferencedError`,
|
|
639
|
+
`MemberNameTakenError`, `OwnerNotFoundError`, `OverlappingEditsError` and the
|
|
640
|
+
rest of the `EditError` family, each carrying `failure`, `diagnostics`,
|
|
641
|
+
`referringElements` and structured `referrers`.
|
|
642
|
+
|
|
643
|
+
## Service version and required capabilities
|
|
644
|
+
|
|
645
|
+
`connect()` negotiates what the service must be, beside what it must advertise:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
await using connection = await connect({
|
|
649
|
+
version: "0.9",
|
|
650
|
+
requireCapabilities: [CAPABILITY_VERIFY, CAPABILITY_DOCUMENT_QUERY],
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
A running service that reports another version is a `StaleServiceError`
|
|
655
|
+
naming the required and reported versions — the remedy is to point at a service
|
|
656
|
+
of the required version, or accept what is running by omitting `version`
|
|
657
|
+
(`$OPENSYSML_GRPC_VERSION` is the environment form). A service missing a required
|
|
658
|
+
capability is likewise refused at connect, rather than at the first gated call.
|
|
659
|
+
|
|
660
|
+
## Typed module generation
|
|
661
|
+
|
|
662
|
+
`opensysml-generate` writes a TypeScript module of typed views over a model's
|
|
663
|
+
definitions — `Vehicle`, `SportsCar` — so `model.instantiate` reads as
|
|
664
|
+
`vehicle.wheels`, with each feature's declared type, instead of
|
|
665
|
+
`tree.get("wheels")`:
|
|
666
|
+
|
|
667
|
+
```bash
|
|
668
|
+
npx opensysml-generate model.sysml -o src/vehicle_types.ts
|
|
669
|
+
npx opensysml-generate model.sysml --check -o src/vehicle_types.ts # fails when stale
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
import { Vehicle } from "./vehicle_types.js";
|
|
674
|
+
const vehicle = Vehicle.fromInstance(await model.instantiate("Demo::Car"), tree.byId);
|
|
675
|
+
vehicle.wheels; // readonly Instance[], typed
|
|
676
|
+
vehicle.mass; // Quantity | undefined, as declared
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
A class `extends` its first base, declares the features of the rest as getters
|
|
680
|
+
so every inherited feature stays reachable, and `fromInstance` accepts
|
|
681
|
+
subtypes; a feature of another kind raises `TypeMismatchError` or
|
|
682
|
+
`InstanceTypeError`. The module is stamped with a hash of the source text, and
|
|
683
|
+
`--check` regenerates on drift. Generated code imports only from
|
|
684
|
+
`@openmbee/opensysml` and runs under the browser entry point too.
|
|
685
|
+
|
|
686
|
+
## What this client does not do
|
|
687
|
+
|
|
688
|
+
- `save` writes a single document's content; a multi-document `EditResult`
|
|
689
|
+
(`applied`/`documents` rather than one `content`) is written by the caller,
|
|
690
|
+
per `EditedDocument`.
|
|
691
|
+
- `connection.rpc` remains the escape hatch for anything not wrapped: it is the
|
|
692
|
+
generated Connect client, and `SysMLService` is exported for a caller
|
|
693
|
+
building its own.
|
|
379
694
|
|
|
380
695
|
## Examples
|
|
381
696
|
|
|
@@ -400,9 +715,12 @@ npm run example 03 # one, by number or name
|
|
|
400
715
|
## Conformance
|
|
401
716
|
|
|
402
717
|
The suite in `conformance/` is the service contract, and this client runs it
|
|
403
|
-
**through its public API** — `load`/`loads`, `eval`, `symbol`,
|
|
404
|
-
|
|
405
|
-
|
|
718
|
+
**through its public API** — `load`/`loads`, `parseSources`, `eval`, `symbol`,
|
|
719
|
+
`instantiate`, `executeAction`, `convert`, `migrate`, `applyEdits`, `verifyConstraint`,
|
|
720
|
+
`query`, `runDocumentQuery`, `renderDocument`, `runAnalysis`, `runSweep`,
|
|
721
|
+
`listEngines` — not through the generated stubs. The only scenarios skipped are
|
|
722
|
+
the ones whose request the public API refuses eagerly rather than asking the
|
|
723
|
+
service, and the report has the same shape `tools/cmd/conformance` emits:
|
|
406
724
|
|
|
407
725
|
```bash
|
|
408
726
|
npm run conformance -- --allow-skips --report report.json
|
|
@@ -410,14 +728,19 @@ npm run conformance -- --allow-skips --report report.json
|
|
|
410
728
|
|
|
411
729
|
| protocol | ran | passed | failed | skipped |
|
|
412
730
|
| --- | --: | --: | --: | --: |
|
|
413
|
-
| `grpc` |
|
|
414
|
-
| `connect` |
|
|
415
|
-
| `connect-json` |
|
|
416
|
-
| **total** | **
|
|
417
|
-
|
|
418
|
-
The
|
|
419
|
-
|
|
420
|
-
`loads` always name one
|
|
731
|
+
| `grpc` | 166 | 163 | 0 | 3 |
|
|
732
|
+
| `connect` | 166 | 163 | 0 | 3 |
|
|
733
|
+
| `connect-json` | 166 | 163 | 0 | 3 |
|
|
734
|
+
| **total** | **498** | **489** | **0** | **9** |
|
|
735
|
+
|
|
736
|
+
The 3 skips per protocol are the requests the public API cannot express because
|
|
737
|
+
it validates them before calling: a `ParseFile` naming no source (`load` and
|
|
738
|
+
`loads` always name one), a `ParseSources` naming no document and a
|
|
739
|
+
`ParseSources` naming two documents alike (`parseSources` refuses both). Every
|
|
740
|
+
skip carries its reason in the report. A refusal the client makes before asking,
|
|
741
|
+
in the service's own words and with the status the service would answer — a v1
|
|
742
|
+
model offered to `convert`, a v2 one to `migrate` — is reported as that status,
|
|
743
|
+
so those scenarios run rather than skip.
|
|
421
744
|
|
|
422
745
|
**The runner is not vacuous.** `--mutate <name>` corrupts a response on its way
|
|
423
746
|
through the client, and each mutation makes at least one scenario fail:
|
|
@@ -452,10 +775,9 @@ commands, plus the mutation checks and a stub-drift check, in both
|
|
|
452
775
|
|
|
453
776
|
## Release
|
|
454
777
|
|
|
455
|
-
|
|
456
|
-
[docs/project/releasing.md](../../docs/project/releasing.md)
|
|
457
|
-
@openmbee/opensysml to npm".
|
|
458
|
-
publishes this package and its five per-platform packages at the version
|
|
778
|
+
The package and its five per-platform `sysml-grpc` packages are published to npm with each core
|
|
779
|
+
`v*` release. The procedure is in [docs/project/releasing.md](../../docs/project/releasing.md)
|
|
780
|
+
under "Releasing @openmbee/opensysml to npm". The `release` workflow publishes at the version
|
|
459
781
|
`client/python/opensysml/_version.py` declares, carrying the release's own
|
|
460
782
|
`sysml-grpc` binaries. The granular npm token it needs already lives in the
|
|
461
783
|
`npm` context; granular tokens expire after at most 90 days, so rotation before
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { Connection } from "../core/connection.js";
|
|
2
2
|
import type { TransportOptions } from "../core/connection.js";
|
|
3
3
|
export * from "../core/index.js";
|
|
4
|
+
export { connectWasm } from "./wasm.js";
|
|
5
|
+
export type { BrowserWasmConnectOptions } from "./wasm.js";
|
|
4
6
|
/** How a browser connects: the address is required, because nothing can be started. */
|
|
5
7
|
export interface BrowserConnectOptions extends TransportOptions {
|
|
6
8
|
address: string;
|
|
9
|
+
/**
|
|
10
|
+
* Release tag the service must report. 'latest' cannot be resolved in a
|
|
11
|
+
* browser, so it requires nothing.
|
|
12
|
+
*/
|
|
13
|
+
version?: string;
|
|
14
|
+
/** Capabilities the service must report for the connection to be returned. */
|
|
15
|
+
requireCapabilities?: readonly string[];
|
|
7
16
|
}
|
|
8
17
|
/**
|
|
9
18
|
* Connects to a running service. The service must allow this page's exact origin
|
package/dist/browser/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Connection } from "../core/connection.js";
|
|
|
5
5
|
import { OpenSysMLError } from "../core/errors.js";
|
|
6
6
|
import { baseUrl, encodingOf, interceptors, timeoutOf } from "../core/transport.js";
|
|
7
7
|
export * from "../core/index.js";
|
|
8
|
+
export { connectWasm } from "./wasm.js";
|
|
8
9
|
/**
|
|
9
10
|
* Connects to a running service. The service must allow this page's exact origin
|
|
10
11
|
* (`-cors-allowed-origins`) and, from an HTTPS page, must be served over TLS.
|
|
@@ -21,6 +22,9 @@ export async function connect(options) {
|
|
|
21
22
|
useBinaryFormat: encoding === "protobuf",
|
|
22
23
|
interceptors: interceptors(options),
|
|
23
24
|
});
|
|
25
|
+
const required = options.version === undefined || options.version === "" || options.version === "latest"
|
|
26
|
+
? undefined
|
|
27
|
+
: options.version;
|
|
24
28
|
return Connection.open({
|
|
25
29
|
transport,
|
|
26
30
|
encoding,
|
|
@@ -28,7 +32,19 @@ export async function connect(options) {
|
|
|
28
32
|
origin: url,
|
|
29
33
|
// Nothing to release: the page never owned the service.
|
|
30
34
|
release: () => Promise.resolve(),
|
|
35
|
+
warn: (message) => {
|
|
36
|
+
console.warn(message);
|
|
37
|
+
},
|
|
31
38
|
},
|
|
32
39
|
timeoutMs,
|
|
40
|
+
...(required === undefined ? {} : { requiredVersion: required }),
|
|
41
|
+
...(options.requireCapabilities === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { requiredCapabilities: options.requireCapabilities }),
|
|
44
|
+
stale: {
|
|
45
|
+
address: url,
|
|
46
|
+
remedy: `point this page at a sysml-grpc that reports ${required ?? "a matching release"}, ` +
|
|
47
|
+
`or accept what is running by omitting version`,
|
|
48
|
+
},
|
|
33
49
|
});
|
|
34
50
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Connection } from "../core/connection.js";
|
|
2
|
+
import type { TransportOptions } from "../core/connection.js";
|
|
3
|
+
/** Options for connecting to a Go WebAssembly module in a browser. */
|
|
4
|
+
export interface BrowserWasmConnectOptions extends TransportOptions {
|
|
5
|
+
/** Module URL, response, bytes, or a compiled WebAssembly module. */
|
|
6
|
+
wasm: string | URL | Response | ArrayBuffer | Uint8Array | WebAssembly.Module;
|
|
7
|
+
/** wasm_exec.js from the Go toolchain that built the module. */
|
|
8
|
+
wasmExec?: string | URL;
|
|
9
|
+
/** A worker running the package's browser WASM worker entry point. */
|
|
10
|
+
worker?: Worker;
|
|
11
|
+
version?: string;
|
|
12
|
+
requireCapabilities?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Connects to sysml-wasm inline, or in the provided worker.
|
|
16
|
+
*/
|
|
17
|
+
export declare function connectWasm(options: BrowserWasmConnectOptions): Promise<Connection>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Connection } from "../core/connection.js";
|
|
2
|
+
import { connectWasmHost, instantiateInline, loadGoConstructor, loadWasmSource, WorkerWasmHost, } from "../core/wasm.js";
|
|
3
|
+
/**
|
|
4
|
+
* Connects to sysml-wasm inline, or in the provided worker.
|
|
5
|
+
*/
|
|
6
|
+
export async function connectWasm(options) {
|
|
7
|
+
const host = options.worker === undefined
|
|
8
|
+
? await startInline(options.wasm, options.wasmExec)
|
|
9
|
+
: await startWorker(options.worker, options.wasm, options.wasmExec);
|
|
10
|
+
return connectWasmHost(host, options);
|
|
11
|
+
}
|
|
12
|
+
async function startInline(wasm, wasmExec) {
|
|
13
|
+
const module = await browserWasmSource(wasm);
|
|
14
|
+
const Go = await loadGoConstructor(wasmExec === undefined ? undefined : resourceUrl(wasmExec));
|
|
15
|
+
return instantiateInline(module, Go);
|
|
16
|
+
}
|
|
17
|
+
async function startWorker(worker, wasm, wasmExec) {
|
|
18
|
+
const host = new WorkerWasmHost(browserWorkerEndpoint(worker));
|
|
19
|
+
try {
|
|
20
|
+
const { source, transfer } = await workerSource(wasm);
|
|
21
|
+
return await host.start({
|
|
22
|
+
type: "init",
|
|
23
|
+
wasm: source,
|
|
24
|
+
...(wasmExec === undefined ? {} : { wasmExec: resourceUrl(wasmExec) }),
|
|
25
|
+
}, transfer);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
await host.close();
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function browserWorkerEndpoint(worker) {
|
|
33
|
+
return {
|
|
34
|
+
post(message, transfer) {
|
|
35
|
+
worker.postMessage(message, [...transfer]);
|
|
36
|
+
},
|
|
37
|
+
onMessage(listener) {
|
|
38
|
+
worker.addEventListener("message", (event) => {
|
|
39
|
+
listener(event.data);
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
onFailure(listener) {
|
|
43
|
+
worker.addEventListener("error", (event) => {
|
|
44
|
+
listener(new Error(event.message || "the sysml-wasm worker failed"));
|
|
45
|
+
});
|
|
46
|
+
worker.addEventListener("messageerror", () => {
|
|
47
|
+
listener(new Error("the sysml-wasm worker sent an unreadable message"));
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
terminate() {
|
|
51
|
+
worker.terminate();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function workerSource(wasm) {
|
|
56
|
+
if (wasm instanceof WebAssembly.Module) {
|
|
57
|
+
return { source: wasm, transfer: [] };
|
|
58
|
+
}
|
|
59
|
+
if (wasm instanceof Response) {
|
|
60
|
+
if (wasm.bodyUsed) {
|
|
61
|
+
throw new Error("the WebAssembly Response body has already been read");
|
|
62
|
+
}
|
|
63
|
+
const bytes = await wasm.arrayBuffer();
|
|
64
|
+
return { source: bytes, transfer: [bytes] };
|
|
65
|
+
}
|
|
66
|
+
if (wasm instanceof ArrayBuffer) {
|
|
67
|
+
const bytes = wasm.slice(0);
|
|
68
|
+
return { source: bytes, transfer: [bytes] };
|
|
69
|
+
}
|
|
70
|
+
if (wasm instanceof Uint8Array) {
|
|
71
|
+
const bytes = Uint8Array.from(wasm).buffer;
|
|
72
|
+
return { source: bytes, transfer: [bytes] };
|
|
73
|
+
}
|
|
74
|
+
const url = wasm instanceof URL ? wasm.href : resourceUrl(wasm);
|
|
75
|
+
return { source: url, transfer: [] };
|
|
76
|
+
}
|
|
77
|
+
async function browserWasmSource(wasm) {
|
|
78
|
+
if (wasm instanceof WebAssembly.Module ||
|
|
79
|
+
wasm instanceof ArrayBuffer ||
|
|
80
|
+
wasm instanceof Uint8Array) {
|
|
81
|
+
return wasm;
|
|
82
|
+
}
|
|
83
|
+
if (wasm instanceof Response && wasm.bodyUsed) {
|
|
84
|
+
throw new Error("the WebAssembly Response body has already been read");
|
|
85
|
+
}
|
|
86
|
+
return loadWasmSource(wasm instanceof Response ? wasm : wasm instanceof URL ? wasm : resourceUrl(wasm));
|
|
87
|
+
}
|
|
88
|
+
function resourceUrl(value) {
|
|
89
|
+
if (value instanceof URL) {
|
|
90
|
+
return value.href;
|
|
91
|
+
}
|
|
92
|
+
const base = typeof document === "undefined" ? import.meta.url : document.baseURI;
|
|
93
|
+
return new URL(value, base).href;
|
|
94
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Refuses `value` that is not a string, naming the parameter it was passed as. */
|
|
2
|
+
export declare function requireString(param: string, value: unknown): asserts value is string;
|
|
3
|
+
/** Refuses `value` that is not a string, or one whose lone surrogates cannot be sent. */
|
|
4
|
+
export declare function requireSourceText(param: string, value: unknown): asserts value is string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Guards the public entry points reach before any call: a non-string argument
|
|
2
|
+
// is refused as a TypeError, and source text a lone UTF-16 surrogate makes
|
|
3
|
+
// unsendable is refused as a RangeError, as Python's client refuses them.
|
|
4
|
+
// One half of a surrogate pair without the other (a high surrogate followed by
|
|
5
|
+
// anything but a low surrogate, or a low surrogate not led by a high one).
|
|
6
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
7
|
+
/** Refuses `value` that is not a string, naming the parameter it was passed as. */
|
|
8
|
+
export function requireString(param, value) {
|
|
9
|
+
if (typeof value !== "string") {
|
|
10
|
+
throw new TypeError(`${param} must be a string, got ${value === null ? "null" : typeof value}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Refuses `value` that is not a string, or one whose lone surrogates cannot be sent. */
|
|
14
|
+
export function requireSourceText(param, value) {
|
|
15
|
+
requireString(param, value);
|
|
16
|
+
if (LONE_SURROGATE.test(value)) {
|
|
17
|
+
throw new RangeError(`${param} contains a lone UTF-16 surrogate, which cannot be sent to the service`);
|
|
18
|
+
}
|
|
19
|
+
}
|