@openmbee/opensysml 0.9.1

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 ADDED
@@ -0,0 +1,462 @@
1
+ # @openmbee/opensysml
2
+
3
+ Node and browser client for OpenSysML: parse, inspect and instantiate SysML v2
4
+ models over the `sysml-grpc` service, using the [Connect
5
+ protocol](https://connectrpc.com/docs/protocol) with protobuf bodies. No native
6
+ addon, so an install is a plain registry fetch.
7
+
8
+ ```bash
9
+ npm install @openmbee/opensysml # from npm, once the first release is published
10
+ ```
11
+
12
+ ```ts
13
+ import { loads, connect } from "@openmbee/opensysml";
14
+
15
+ await using model = await loads(`package Demo {
16
+ part def Wheel { attribute radius : ScalarValues::Real = 0.3; }
17
+ part def Car { part wheels : Wheel[4]; attribute mass : ScalarValues::Real = 1500.0; }
18
+ }`);
19
+
20
+ const value = await model.eval("2 + 2"); // { kind: "int", value: 4n }
21
+ const car = await model.symbol("Demo::Car"); // by qualified name
22
+ await car.children(); // its members, as symbols
23
+
24
+ const tree = await model.instantiate("Demo::Car");
25
+ const wheels = tree.get("wheels"); // a FeatureValue union
26
+ if (wheels?.kind === "many") {
27
+ console.log(wheels.values.length); // 4
28
+ }
29
+ ```
30
+
31
+ `loads`/`load` open a connection of their own and close it with the model.
32
+ `connect()` is the longer-lived form, and parses more than one source over one
33
+ service and one parse cache:
34
+
35
+ ```ts
36
+ await using connection = await connect();
37
+ const first = await connection.load("model.sysml");
38
+ const second = await connection.loads("package Inline { part def Thing; }");
39
+ connection.info.capabilities; // what this service can do
40
+ connection.model(first.hash); // a model the service already holds
41
+ ```
42
+
43
+ Both `Connection` and `Model` implement `Symbol.asyncDispose`, so `await using`
44
+ closes them; `close()` is the explicit form, and is safe to call twice.
45
+
46
+ ## Values are discriminated unions
47
+
48
+ Every `oneof` the service answers with arrives as a union to switch on, rather
49
+ than a generated message with optional fields:
50
+
51
+ ```ts
52
+ switch (value.kind) {
53
+ case "int": value.value; // bigint, never lossy
54
+ case "real": value.value; // number
55
+ case "complex": value.value.real; value.value.imaginary; // 1.5 - 2.0i, one value
56
+ case "boolean": value.value;
57
+ case "string": value.value;
58
+ case "quantity": value.magnitude; value.unit; // 1500.0 [kg]
59
+ case "measurementRef": value.unit; value.unitTerm; value.unitId; // a bare unit: km, reduced to 1000·metre
60
+ case "function": value.calcId; value.selfId; // a calc as a value: Demo::Sq, or holder.scale read off an object
61
+ case "array": value.dimensions; value.elements; // row-major, an element is any SysMLValue
62
+ case "vector": value.components; // { kind: "int" | "real" }[]
63
+ case "vectorQuantity": value.components; // QuantityValue[], a unit per component
64
+ case "set": value.elements; // SysMLValue[], each once, unordered
65
+ case "tensorQuantity": value.dimensions; value.components; // any rank, row-major QuantityValue[]
66
+ case "metaobject": value.elementId; value.metaclassId; // an element reflected on: x meta KerML::Feature
67
+ case "enum": value.value.name; value.value.value; // its literal/enumeration ids, and the scalar a `high = 3` literal carries
68
+ case "instance": value.id; // an object in the same tree
69
+ case "sequence": value.elements; // SysMLValue[]
70
+ case "undetermined": value.reason; value.countLower; value.countUpper; // the model leaves it open
71
+ case "infinity": break; // the unbounded `*`
72
+ case "null": value.reason; // evaluated, no value
73
+ case "unset": break; // declared, never given one
74
+ case "absent": break; // the service sent no value at all
75
+ }
76
+ ```
77
+
78
+ `unset`, `undetermined` and `absent` are distinct on purpose: the first is a feature the
79
+ model leaves without a value, the second a model-level answer the model leaves open (an
80
+ unbound feature, a count the multiplicity does not fix) that is read but never sent, and the
81
+ third a field the answer did not carry.
82
+ `SysMLVerdict` (`holds` / `fails` / `undecided`) and `FeatureValue` (`single` /
83
+ `many` / `error`) are unions of the same shape; every verdict arm carries a `standing` — the
84
+ engine that answered, the strength of its evidence (`observed`, `witnessed`, `bounded`,
85
+ `proved`) and the bounds it ran under — empty from a service without the `engines`
86
+ capability. Integers are `bigint`, because
87
+ the service's `int64` does not fit a `number` — an exact comparison against a
88
+ scenario expectation would otherwise be a lie.
89
+
90
+ ## Two lifecycle modes
91
+
92
+ ### A private child of this process (the default)
93
+
94
+ `connect()` with no address starts `sysml-grpc` as a child of this process with
95
+ `-port 0 -health-port 0 -report-address -exit-with-parent`, and reads the address
96
+ it bound from the child's first stdout line. No port is chosen, probed or
97
+ retried, so two processes starting at once cannot collide.
98
+
99
+ One child serves **every connection of a thread**: the first `connect()` starts
100
+ it, the last `close()` stops it, and sharing it shares the service's parse cache.
101
+ It is per thread rather than per process because the module state that holds it
102
+ is per thread — a `worker_threads` worker gets its own child, and closing the
103
+ worker's connections stops that child alone.
104
+
105
+ **No orphans, and the mechanism is not an exit hook.** The client holds the write
106
+ end of the child's stdin pipe and never writes to it; the child exits at end of
107
+ file. The kernel closes that pipe when the holder dies however it dies, which
108
+ survives what a `process.on("exit")` hook does not: `SIGKILL`, `process.abort()`,
109
+ an uncaught fatal error, a crash during shutdown.
110
+ `test/orphan.test.ts` proves it by `SIGKILL`ing a parent that holds a connection
111
+ and asserting the child is gone.
112
+
113
+ Node adds three wrinkles Python does not have, all deliberate here:
114
+
115
+ - **The event loop.** A referenced child handle keeps Node alive, so a script that
116
+ forgot `close()` would never exit. The child and its three stdio handles are
117
+ `unref()`ed as soon as the address arrives — before that they stay referenced,
118
+ or Node could exit in the middle of starting the service. `stop()` `ref()`s the
119
+ child again for as long as it waits for it to exit.
120
+ - **`detached: true`.** The child is started in a session of its own, so a
121
+ `SIGINT` meant for this process does not reach it mid-call; stdin is what ends
122
+ it. It is never `unref`ed *and* left running: this process holds the only write
123
+ end of its stdin.
124
+ - **Worker threads.** Each thread owns its own child, as above.
125
+
126
+ On **Windows** the guarantee is the same and rests on the same mechanism: the OS
127
+ closes the anonymous stdin pipe when the owning process exits, however it exits,
128
+ so the child sees end of file. What differs is that there is no process group to
129
+ signal and no `SIGKILL` — `child.kill()` is `TerminateProcess` — so the orphan
130
+ test is POSIX-only (`process.kill(pid, 0)` and `SIGKILL` have no equivalent), and
131
+ prompt shutdown on Windows comes from closing stdin rather than from a signal.
132
+
133
+ ### A service someone else runs (explicit opt-in)
134
+
135
+ ```ts
136
+ const connection = await connect({ address: "localhost:50051" });
137
+ await connection.close(); // that service keeps running
138
+ ```
139
+
140
+ or set `$OPENSYSML_SERVICE=host:port`. A connection made this way never owns the
141
+ service: closing it disconnects and nothing else. There is no adoption of a
142
+ service left listening by another process, no pidfile and no port probing.
143
+
144
+ ## The browser
145
+
146
+ ```ts
147
+ import { connect } from "@openmbee/opensysml/browser";
148
+
149
+ await using connection = await connect({ address: "https://sysml.example.com" });
150
+ ```
151
+
152
+ The browser entry point is the **explicit-address path only**: a browser cannot
153
+ spawn a process, so there is no private child there and nothing to fall back to.
154
+ It uses `@connectrpc/connect-web`, which is `fetch` and needs no proxy and no
155
+ sidecar.
156
+
157
+ Two limits to plan for rather than discover:
158
+
159
+ - The service must allow the page's **exact origin** — start it with
160
+ `-cors-allowed-origins https://app.example.com`, never `*` — and must be
161
+ served over TLS (`-tls-cert`/`-tls-key`) for an HTTPS page to reach it.
162
+ - `connect-go` v1.20 does not implement the base64 **`grpc-web-text`** variant.
163
+ This client does not need it: a `fetch`-based Connect client sends and reads
164
+ binary bodies directly. A `grpc-web` client that requires `-text` will not work
165
+ against this service, whatever the client.
166
+
167
+ `test/browser.test.ts` runs this entry point against a real service over the same
168
+ `fetch` transport, and asserts the allowed origin is answered on the preflight
169
+ while another origin is not.
170
+
171
+ ## Protobuf, not JSON
172
+
173
+ Bodies are protobuf by default. JSON is available (`connect({ encoding: "json" })`)
174
+ for `curl`-shaped debugging, and it is the same answers, but
175
+ [`docs/internals/design/transport-evaluation.md`](../../docs/internals/design/transport-evaluation.md)
176
+ measured a 468 KB response at ~6.5 ms with a protobuf body against ~42 ms with
177
+ JSON — `protojson` CPU on the service, not the ~10% difference in bytes. The
178
+ gRPC protocol is also available (`connect({ protocol: "grpc" })`) and carries
179
+ protobuf only; asking for `{ protocol: "grpc", encoding: "json" }` is refused
180
+ rather than silently downgraded.
181
+
182
+ ## Capability negotiation
183
+
184
+ Clients negotiate on the capability names `GetServerInfo` reports, not on
185
+ versions:
186
+
187
+ ```ts
188
+ import { CAPABILITY_EVALUATE_SUBJECT } from "@openmbee/opensysml";
189
+
190
+ if (connection.info.has(CAPABILITY_EVALUATE_SUBJECT)) {
191
+ await model.eval("mass", { subject: "Demo::sedan" });
192
+ }
193
+ ```
194
+
195
+ The client checks the advertised list **before** making such a call so it can
196
+ raise a `MissingCapabilityError` naming the service, its version and the way to
197
+ get one that has it. A direct capability-gated request to a service without the
198
+ capability is refused with `UNIMPLEMENTED`; response-population capabilities
199
+ instead omit the fields they name. A service without `structured_values`,
200
+ `measurement_refs`, `function_values`, `set_values`, `tensor_values` or
201
+ `metaobject_values` sends the value kinds those name (`array`, `vector`,
202
+ `vectorQuantity`; `measurementRef`; `function`; `set`; `tensorQuantity`;
203
+ `metaobject`) as `null` with an `unsupported: …` reason.
204
+ A function closing over the bindings of a behavior body has no wire form and is
205
+ sent as `null` by every service. A `set` arrives
206
+ with its elements in the service's canonical order, so two equal sets arrive
207
+ alike, and one listing a member twice is a `MalformedValueError`, whether it
208
+ arrives or is about to be sent; one sent to the service may list its members in
209
+ any order. `valuesEqual` is the membership test, as the service judges it: sets
210
+ by membership, sequences in order, numbers by value — `1` and `1.0` are one
211
+ member, exactly across the whole `int` range — and a quantity by magnitude
212
+ through its `unitTerm`, so `1 [m]` is `100 [cm]` (exactly, while the magnitude
213
+ is an `int` and the scale a whole ratio); one without a `unitTerm` is compared
214
+ in its unit as written. A `tensorQuantity` carries its `dimensions` and one
215
+ quantity per component, row-major. A `metaobject` — what `x meta KerML::Feature`
216
+ or the last element of `x.metadata` evaluates to — names the element reflected
217
+ on (`elementId`, its identity) and the element's own metaclass (`metaclassId`,
218
+ not the type it was cast to); two are `valuesEqual` exactly when they name one
219
+ element. Its features (`declaredName`, `ownedFeature`, …) are read in the model,
220
+ not carried. One sent to the service may leave `metaclassId` empty to have the
221
+ model's used; one naming a metaclass that is not the element's is refused, and
222
+ one naming no element is a `MalformedValueError`.
223
+
224
+ ## Failures are typed
225
+
226
+ Every failure is an `OpenSysMLError`. A call the service refused is a
227
+ `ServiceError` whose `code` is the RPC status it came back with (`"NOT_FOUND"`,
228
+ `"INVALID_ARGUMENT"`, …), and the statuses worth catching by themselves have a
229
+ subclass: `ModelNotFoundError` (the service no longer holds that hash),
230
+ `ModelFileNotFoundError`, `InvalidRequestError`, `ServiceTimeoutError`,
231
+ `UnsupportedOperationError`. A name the model has not got is a
232
+ `SymbolNotFoundError`, which carries the `symbolName` it looked for and the
233
+ `suggestions` closest to it:
234
+
235
+ ```ts
236
+ try {
237
+ await model.symbol("Wheeel");
238
+ } catch (error) {
239
+ if (error instanceof SymbolNotFoundError) {
240
+ console.error(`no ${error.symbolName}; did you mean ${error.suggestions[0]}?`);
241
+ }
242
+ }
243
+ ```
244
+
245
+ Source that does not parse is not a failure: `load`/`loads` return a model whose
246
+ `hasErrors` is true and whose `diagnostics` say where. Each `ModelDiagnostic` has
247
+ `severity`, `message`, `code` and an optional location; branch on `code`
248
+ (`"syntax"`, a validation code such as `"unresolved"`, `"choice-point"`,
249
+ `"guard-unevaluable"`; `""` when the service assigned none), not on the message
250
+ text. A service that populates `code` advertises `CAPABILITY_DIAGNOSTIC_CODES`;
251
+ without it every code is `""`. Options that cannot work
252
+ (an encoding that is not one, a timeout that cannot elapse, `grpc` with `json`)
253
+ are refused before a connection is opened or a service started.
254
+
255
+ ## The service binary
256
+
257
+ The binary comes from an **optional per-platform npm package**, selected by npm
258
+ from its `os`/`cpu` metadata:
259
+
260
+ | package | platform |
261
+ | --- | --- |
262
+ | `@openmbee/opensysml-sysml-grpc-linux-x64` | Linux x86-64 |
263
+ | `@openmbee/opensysml-sysml-grpc-linux-arm64` | Linux arm64 |
264
+ | `@openmbee/opensysml-sysml-grpc-darwin-x64` | macOS Intel |
265
+ | `@openmbee/opensysml-sysml-grpc-darwin-arm64` | macOS Apple silicon |
266
+ | `@openmbee/opensysml-sysml-grpc-win32-x64` | Windows x86-64 |
267
+
268
+ That is a normal registry install: npm verifies the tarball against the
269
+ registry's integrity hash, and **there is no postinstall script**, so a platform
270
+ with a package never downloads anything.
271
+
272
+ Resolution order:
273
+
274
+ 1. `$OPENSYSML_BINARY` — a path to a binary, which wins over everything;
275
+ 2. the platform package above;
276
+ 3. `~/.opensysml/bin/sysml-grpc`, the cache the Python client also uses, filled
277
+ by a verified download of a release when nothing above resolved;
278
+ 4. `sysml-grpc` on `$PATH`;
279
+ 5. otherwise: an error, or connect to a service someone else runs.
280
+
281
+ ### Downloading a release
282
+
283
+ `resolveBinary()` downloads a `sysml-grpc-<os>-<arch>` release asset into
284
+ `~/.opensysml/bin/sysml-grpc` (`.exe` on Windows) when the steps above resolved
285
+ nothing — the same cache, the same metadata beside it in `sysml-grpc.json`, and
286
+ the same trust model as the Python client, so either client can use what the
287
+ other downloaded. `process.platform`/`process.arch` map to the five published
288
+ pairs (`linux-amd64`, `linux-arm64`, `darwin-amd64`, `darwin-arm64`,
289
+ `windows-amd64`); any other pair is an error naming it rather than a fetch.
290
+
291
+ | variable | effect |
292
+ | --- | --- |
293
+ | `$OPENSYSML_BINARY` | a path to a binary, which wins over everything |
294
+ | `$OPENSYSML_GRPC_VERSION` | the release to download, else `latest` from the releases API |
295
+ | `$OPENSYSML_GITHUB_REPO` | the release repository, default `Open-MBEE/OpenSysML` |
296
+ | `$OPENSYSML_ALLOW_UNPINNED_DOWNLOAD` | `<owner/repo>`, or `1` for any repository: accept same-origin trust |
297
+
298
+ The download goes to a temporary file, is hashed, and only then replaces the
299
+ cache path atomically and is `chmod 0700`ed (POSIX); a download that does not
300
+ verify leaves an existing cached binary and its metadata untouched and removes
301
+ the temporary file. A cached binary of another version is replaced with a
302
+ warning rather than used, and every request times out after 15 seconds. A
303
+ transport failure falls back to a cached binary that still verifies; a digest or
304
+ signature failure never does.
305
+
306
+ ### What a download is verified against
307
+
308
+ In order, and each step refuses rather than falling back to the next:
309
+
310
+ 1. **A shipped pin.** `release-digests.json`, synced from
311
+ `client/release-digests.json` by `python3 scripts/sync-release-digests.py`
312
+ and published in the tarball, pins the SHA-256 of every asset of a release.
313
+ Where it pins one, that is what the bytes must hash to, and a served
314
+ `.sha256` that disagrees is tampering: the download fails.
315
+ 2. **The release's signed manifest.** With no pin, the client downloads
316
+ `SHA256SUMS.txt` and its sigstore bundle `SHA256SUMS.txt.bundle`, verifies
317
+ the bundle against the release pipeline's certificate identity (the CircleCI
318
+ OIDC issuer and project in `src/node/signing.ts`), and takes the digest from
319
+ the verified manifest. Anything short of that — no bundle, a signature that
320
+ does not verify, another signer, an expired certificate, a manifest changed
321
+ after signing, a repository with no known signer, or the optional sigstore
322
+ packages not installed — is refused exactly as an unpinned release is. A
323
+ manifest digest that contradicts a pin is an error, not a downgrade.
324
+ 3. **Nothing.** The download fails naming the version, because the `.sha256`
325
+ served beside a binary comes from whoever served the binary: it detects
326
+ corruption but not a compromised release.
327
+ `$OPENSYSML_ALLOW_UNPINNED_DOWNLOAD=<owner/repo>` (or `=1` for any
328
+ repository) accepts that same-origin trust explicitly, with a warning saying
329
+ so. It is never a way around a failed signature or a pin mismatch.
330
+
331
+ Verification uses `@sigstore/verify` with `@sigstore/bundle`,
332
+ `@sigstore/protobuf-specs` and `@sigstore/tuf` — the packages the `sigstore`
333
+ package is itself built from — as **optional** dependencies, so the client
334
+ installs and works without them and a release with no pin is refused where they
335
+ are missing. They are used rather than `sigstore.verify` because that entry
336
+ point takes its trusted root only through TUF, and both the Python client and
337
+ these tests verify against a recorded trusted root offline.
338
+
339
+ The per-platform packages are built by
340
+ `npm run platform-packages -- --binaries <dir>`, and each binary is packaged
341
+ **only** if its bytes match the `.sha256` sidecar beside it; a missing sidecar is
342
+ refused rather than trusted. The release job cross-compiles those binaries from
343
+ the tagged revision in the same run that publishes the packages and writes the
344
+ sidecars there, so nothing is downloaded to authenticate — which is the job the
345
+ Python client's pinned digests do — and an install is a normal registry fetch
346
+ carrying npm's own integrity hashes. npm's `--provenance` is not used because
347
+ the CLI mints attestations only on GitHub Actions and GitLab CI/CD, and this
348
+ repository releases from CircleCI. The README of each package records the digest
349
+ of the binary it carries.
350
+
351
+ ## Generated stubs
352
+
353
+ `src/generated/sysml_pb.ts` is generated by `buf` from `api/proto/sysml.proto`
354
+ through the `protoc-gen-es` entry in `api/proto/buf.gen.ts.yaml`. The plugin is this
355
+ package's devDependency, pinned in `package-lock.json` at the same version as the
356
+ `@bufbuild/protobuf` runtime the stubs import, so `make proto` regenerates them
357
+ from `node_modules` with no hand steps and no network fetch at generation time.
358
+
359
+ **The stubs are committed**, for the same reason the Python ones are: `npm
360
+ install @openmbee/opensysml` must not need `buf`, Go or a network fetch of a
361
+ plugin, and a published tarball has to contain the compiled output. CI runs
362
+ `make proto-ts` and fails on any diff, so committed and generated cannot drift.
363
+
364
+ ## What v1 does not do
365
+
366
+ This client covers `GetServerInfo`, `ParseFile`, `GetSymbol`, `Evaluate` and
367
+ `Instantiate` — connection, lifecycle, capability negotiation, and the five RPCs
368
+ above. Deliberately **not** in v1, rather than half-implemented:
369
+
370
+ - generated model-ergonomics types (`python -m opensysml.generate`'s equivalent);
371
+ - the edit API (`ApplyEdits`);
372
+ - RDF conversion (`Convert`);
373
+ - verification helpers (`VerifyConstraint`, `VerifyRequirement`,
374
+ `VerifySatisfaction`);
375
+ - `Query`, `GetDiagnostics`, `EvaluateCalc`, `RunAnalysis`, `ExecuteAction`, `ExecuteState`.
376
+
377
+ `connection.rpc` is the escape hatch: it is the generated Connect client, so any
378
+ RPC not covered here can still be called, without the ergonomic layer.
379
+
380
+ ## Examples
381
+
382
+ `examples/` is six programs against one model, a rover, in `examples/model.ts`.
383
+ They are written to be read in order, they assert what they print, and the test
384
+ suite runs every one of them, so they cannot drift from the API.
385
+
386
+ ```bash
387
+ npm run examples # all of them, in order
388
+ npm run example 03 # one, by number or name
389
+ ```
390
+
391
+ | example | what it shows |
392
+ | --- | --- |
393
+ | `01-tour` | connect, parse, look up, evaluate, instantiate |
394
+ | `02-values` | every value kind, and what it decodes to in JavaScript |
395
+ | `03-symbols` | walking, lookup by name and id, type facts, adoption by hash |
396
+ | `04-instances` | instance trees, single and repeated features, unset and absent |
397
+ | `05-diagnostics` | syntax errors, the error each failure raises, refused options |
398
+ | `06-connections` | ownership, an external service, both protocols, calls at once |
399
+
400
+ ## Conformance
401
+
402
+ The suite in `conformance/` is the service contract, and this client runs it
403
+ **through its public API** — `load`/`loads`, `eval`, `symbol`, `instantiate` —
404
+ not through the generated stubs. A scenario whose RPC v1 does not cover is
405
+ skipped with a reason, and the report has the same shape `tools/cmd/conformance` emits:
406
+
407
+ ```bash
408
+ npm run conformance -- --allow-skips --report report.json
409
+ ```
410
+
411
+ | protocol | ran | passed | failed | skipped |
412
+ | --- | --: | --: | --: | --: |
413
+ | `grpc` | 59 | 23 | 0 | 36 |
414
+ | `connect` | 59 | 23 | 0 | 36 |
415
+ | `connect-json` | 59 | 23 | 0 | 36 |
416
+ | **total** | **177** | **69** | **0** | **108** |
417
+
418
+ The 36 skips per protocol are 35 scenarios for the 10 RPCs listed above plus one
419
+ the public API cannot express: a `ParseFile` naming no source, since `load` and
420
+ `loads` always name one. Every skip carries its reason in the report.
421
+
422
+ **The runner is not vacuous.** `--mutate <name>` corrupts a response on its way
423
+ through the client, and each mutation makes at least one scenario fail:
424
+
425
+ | `--mutate` | what it breaks | caught by |
426
+ | --- | --- | --- |
427
+ | `hide-capability` | drops a capability from `GetServerInfo` | `server_info/...` |
428
+ | `drop-diagnostics` | drops parse diagnostics | `parse/a_syntax_error_...` |
429
+ | `blank-symbol-kind` | blanks `SymbolInfo.kind` | `symbol/...` |
430
+ | `shift-integer` | adds 1 to an integer result | `evaluate/arithmetic_is_an_integer` |
431
+ | `drop-feature-values` | empties an instance's feature values | `instantiate/...` |
432
+
433
+ ```bash
434
+ npm run conformance -- --allow-skips --mutate shift-integer # must fail
435
+ ```
436
+
437
+ ## Development
438
+
439
+ ```bash
440
+ npm install
441
+ npm run build # dist/, what is published
442
+ npm run typecheck
443
+ npm run lint
444
+ npm test # unit, lifecycle and service-backed tests
445
+ npm run conformance -- --allow-skips
446
+ ```
447
+
448
+ The service-backed tests build `sysml-grpc` from this checkout on first use, or
449
+ use `$OPENSYSML_BINARY` when it names one. The `node-test` job runs all five
450
+ commands, plus the mutation checks and a stub-drift check, in both
451
+ `.github/workflows/pr.yml` (pull requests) and `.circleci/config.yml`.
452
+
453
+ ## Release
454
+
455
+ Nothing here is published yet; the procedure is in
456
+ [docs/project/releasing.md](../../docs/project/releasing.md) under "Releasing
457
+ @openmbee/opensysml to npm". In short: the core `v*` tag's `release` workflow
458
+ publishes this package and its five per-platform packages at the version
459
+ `client/python/opensysml/_version.py` declares, carrying the release's own
460
+ `sysml-grpc` binaries. The granular npm token it needs already lives in the
461
+ `npm` context; granular tokens expire after at most 90 days, so rotation before
462
+ a release is a standing task.
@@ -0,0 +1,12 @@
1
+ import { Connection } from "../core/connection.js";
2
+ import type { TransportOptions } from "../core/connection.js";
3
+ export * from "../core/index.js";
4
+ /** How a browser connects: the address is required, because nothing can be started. */
5
+ export interface BrowserConnectOptions extends TransportOptions {
6
+ address: string;
7
+ }
8
+ /**
9
+ * Connects to a running service. The service must allow this page's exact origin
10
+ * (`-cors-allowed-origins`) and, from an HTTPS page, must be served over TLS.
11
+ */
12
+ export declare function connect(options: BrowserConnectOptions): Promise<Connection>;
@@ -0,0 +1,34 @@
1
+ // Browser entry point: fetch-based Connect, explicit address only. A browser
2
+ // cannot spawn a service, so there is no private-child path here.
3
+ import { createConnectTransport } from "@connectrpc/connect-web";
4
+ import { Connection } from "../core/connection.js";
5
+ import { OpenSysMLError } from "../core/errors.js";
6
+ import { baseUrl, encodingOf, interceptors, timeoutOf } from "../core/transport.js";
7
+ export * from "../core/index.js";
8
+ /**
9
+ * Connects to a running service. The service must allow this page's exact origin
10
+ * (`-cors-allowed-origins`) and, from an HTTPS page, must be served over TLS.
11
+ */
12
+ export async function connect(options) {
13
+ if (options.address.trim() === "") {
14
+ throw new OpenSysMLError("a browser cannot start a sysml-grpc service, so connect() needs the address of one");
15
+ }
16
+ const url = baseUrl(options.address);
17
+ const encoding = encodingOf(options);
18
+ const timeoutMs = timeoutOf(options);
19
+ const transport = createConnectTransport({
20
+ baseUrl: url,
21
+ useBinaryFormat: encoding === "protobuf",
22
+ interceptors: interceptors(options),
23
+ });
24
+ return Connection.open({
25
+ transport,
26
+ encoding,
27
+ backend: {
28
+ origin: url,
29
+ // Nothing to release: the page never owned the service.
30
+ release: () => Promise.resolve(),
31
+ },
32
+ timeoutMs,
33
+ });
34
+ }
@@ -0,0 +1,94 @@
1
+ import { OpenSysMLError } from "./errors.js";
2
+ /** Static type facts on a symbol: `typeInfo`, `multiplicity`, `specializations`. */
3
+ export declare const CAPABILITY_TYPE_FACTS = "type_facts";
4
+ /** Populated `SymbolInfo.attributes`. */
5
+ export declare const CAPABILITY_SYMBOL_ATTRIBUTES = "symbol_attributes";
6
+ /** Evaluating an expression against an instantiated subject. */
7
+ export declare const CAPABILITY_EVALUATE_SUBJECT = "evaluate_subject";
8
+ /** An object's values as `Instance.feature_values`. */
9
+ export declare const CAPABILITY_FEATURE_VALUES = "feature_values";
10
+ /** An enumeration literal as `Value.enum_literal`. */
11
+ export declare const CAPABILITY_ENUM_VALUES = "enum_values";
12
+ /** A valueless feature of a value type as `Value.unset`. */
13
+ export declare const CAPABILITY_UNSET_VALUE = "unset_value";
14
+ /** A complex number as `Value.complex`, rather than an unsupported null. */
15
+ export declare const CAPABILITY_COMPLEX_VALUES = "complex_values";
16
+ /** An array, a vector and a vector quantity as their own arms, rather than unsupported nulls. */
17
+ export declare const CAPABILITY_STRUCTURED_VALUES = "structured_values";
18
+ /** A bare measurement unit (`SI::m`, `m / s`) as `Value.measurement_ref`, rather than an unsupported null. */
19
+ export declare const CAPABILITY_MEASUREMENT_REFS = "measurement_refs";
20
+ /** A calc held as a value as `Value.function`, named by its declaration, rather than an unsupported null. */
21
+ export declare const CAPABILITY_FUNCTION_VALUES = "function_values";
22
+ /** The unbounded value `*` as `Value.infinity`, rather than an unsupported null. */
23
+ export declare const CAPABILITY_INFINITY_VALUE = "infinity_value";
24
+ /** `Diagnostic.code` is populated, so an empty code is a finding none was assigned. */
25
+ export declare const CAPABILITY_DIAGNOSTIC_CODES = "diagnostic_codes";
26
+ /** A unique, unordered collection (a `Collections::Set`'s elements) as `Value.set`, rather than an unsupported null. */
27
+ export declare const CAPABILITY_SET_VALUES = "set_values";
28
+ /** A tensor quantity of any rank as `Value.tensor_quantity`, rather than an unsupported null. */
29
+ export declare const CAPABILITY_TENSOR_VALUES = "tensor_values";
30
+ /** An element reflected on (`x meta T`, the last of `x.metadata`) as `Value.metaobject`, rather than an unsupported null. */
31
+ export declare const CAPABILITY_METAOBJECT_VALUES = "metaobject_values";
32
+ /** `finalTime` on an execution response: the run's simulation clock when it ended, in seconds. */
33
+ export declare const CAPABILITY_FINAL_TIME = "final_time";
34
+ /** `ParseFileRequest.language`, which declares the language of inline content. */
35
+ export declare const CAPABILITY_INLINE_LANGUAGE = "inline_language";
36
+ /** `ParseFileRequest.strict_conformance`. */
37
+ export declare const CAPABILITY_STRICT_CONFORMANCE = "strict_conformance";
38
+ /** The `Convert` RPC. Not used by this version; see the README. */
39
+ export declare const CAPABILITY_CONVERT = "convert";
40
+ /** The verification RPCs. Not used by this version; see the README. */
41
+ export declare const CAPABILITY_VERIFICATION = "verification";
42
+ /** What the body of a verification case answered, as `verification_verdicts`. */
43
+ export declare const CAPABILITY_VERIFICATION_VERDICTS = "verification_verdicts";
44
+ /** `RunAnalysisResponse.evaluations`: each call the run made to a calc held as a value, such as a trade study's evaluation of every alternative. */
45
+ export declare const CAPABILITY_CASE_EVALUATIONS = "case_evaluations";
46
+ /** The `Query` RPC. Not used by this version; see the README. */
47
+ export declare const CAPABILITY_QUERY = "query";
48
+ /** The `ApplyEdits` RPC. Not used by this version; see the README. */
49
+ export declare const CAPABILITY_APPLY_EDITS = "apply_edits";
50
+ /** `ApplyEdits` can add `first`/`then` action sequencing members. */
51
+ export declare const CAPABILITY_SEQUENCE_AUTHORING = "sequence_authoring";
52
+ /** `ApplyEdits` can add action-body statements and succession source multiplicities. */
53
+ export declare const CAPABILITY_ACTION_BODY_STATEMENT_AUTHORING = "action_body_statement_authoring";
54
+ /** `ApplyEdits` edits a model of several documents as one batch and answers each edited document by name in `documents`. Not used by this version; see the README. */
55
+ export declare const CAPABILITY_EDIT_DOCUMENTS = "edit_documents";
56
+ /** The `schedule` field of the execution requests, naming the scheduling policy. Not used by this version; see the README. */
57
+ export declare const CAPABILITY_SCHEDULE = "schedule";
58
+ /** The `explore` scheduling policy, answering with every `outcomes` entry and an `exploration` status. Not used by this version; see the README. */
59
+ export declare const CAPABILITY_SCHEDULE_EXPLORE = "schedule_explore";
60
+ /** `performerSymbolId` on the action and state requests: the object the behavior runs on, a declaration or a path from one into its parts. Not used by this version; see the README. */
61
+ export declare const CAPABILITY_PERFORMER = "performer";
62
+ /** The `ListEngines` RPC, the `engine` field selecting an analysis engine, and `engine`, `strength` and `bounds` on the answers. Not used by this version; see the README. */
63
+ export declare const CAPABILITY_ENGINES = "engines";
64
+ /** A model-level result the model leaves open as `Value.undetermined`, read as an `undetermined` value. */
65
+ export declare const CAPABILITY_UNDETERMINED_VALUE = "undetermined_value";
66
+ /** Self-description of the service a connection talks to. */
67
+ export declare class ServerInfo {
68
+ /** Version the service reports. Informational only; empty when unanswered. */
69
+ readonly version: string;
70
+ readonly capabilities: ReadonlySet<string>;
71
+ /** Whether the service answered the handshake; false means it predates `GetServerInfo`. */
72
+ readonly answered: boolean;
73
+ /** Where the service came from: the binary this client started, or the address it dialled. */
74
+ readonly origin: string;
75
+ constructor(init: {
76
+ version: string;
77
+ capabilities: Iterable<string>;
78
+ answered: boolean;
79
+ origin: string;
80
+ });
81
+ has(capability: string): boolean;
82
+ /** One-line description of the service, for an error message. */
83
+ describe(): string;
84
+ }
85
+ /** The connected service does not report a capability the operation requires. */
86
+ export declare class MissingCapabilityError extends OpenSysMLError {
87
+ readonly capability: string;
88
+ readonly info: ServerInfo;
89
+ constructor(capability: string, info: ServerInfo, remedy: string);
90
+ }
91
+ /** Throws unless the service reports `capability`. */
92
+ export declare function requireCapability(info: ServerInfo, capability: string, remedy: string): void;
93
+ /** Remedy text for a service that lacks `capability`, naming both routes to one that has it. */
94
+ export declare function upgradeRemedy(capability: string): string;