@openmbee/opensysml 0.9.2 → 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.
Files changed (67) hide show
  1. package/README.md +355 -33
  2. package/dist/browser/index.d.ts +9 -0
  3. package/dist/browser/index.js +16 -0
  4. package/dist/browser/wasm-worker.d.ts +1 -0
  5. package/dist/browser/wasm-worker.js +2 -0
  6. package/dist/browser/wasm.d.ts +17 -0
  7. package/dist/browser/wasm.js +94 -0
  8. package/dist/core/arguments.d.ts +4 -0
  9. package/dist/core/arguments.js +19 -0
  10. package/dist/core/capabilities.d.ts +90 -9
  11. package/dist/core/capabilities.js +111 -10
  12. package/dist/core/connection.d.ts +203 -2
  13. package/dist/core/connection.js +810 -17
  14. package/dist/core/conversion.d.ts +153 -0
  15. package/dist/core/conversion.js +191 -0
  16. package/dist/core/document.d.ts +188 -0
  17. package/dist/core/document.js +457 -0
  18. package/dist/core/edit.d.ts +446 -0
  19. package/dist/core/edit.js +1994 -0
  20. package/dist/core/engines.d.ts +64 -0
  21. package/dist/core/engines.js +74 -0
  22. package/dist/core/errors.d.ts +164 -2
  23. package/dist/core/errors.js +209 -3
  24. package/dist/core/exploration.d.ts +71 -0
  25. package/dist/core/exploration.js +116 -0
  26. package/dist/core/graphs.d.ts +15 -0
  27. package/dist/core/graphs.js +3 -0
  28. package/dist/core/index.d.ts +30 -9
  29. package/dist/core/index.js +14 -5
  30. package/dist/core/model.d.ts +168 -8
  31. package/dist/core/model.js +258 -31
  32. package/dist/core/package.d.ts +2 -0
  33. package/dist/core/package.js +2 -0
  34. package/dist/core/query.d.ts +50 -0
  35. package/dist/core/query.js +197 -0
  36. package/dist/core/render-view.d.ts +97 -0
  37. package/dist/core/render-view.js +120 -0
  38. package/dist/core/sources.d.ts +34 -0
  39. package/dist/core/sources.js +134 -0
  40. package/dist/core/status.d.ts +9 -2
  41. package/dist/core/status.js +24 -8
  42. package/dist/core/typed.d.ts +71 -0
  43. package/dist/core/typed.js +245 -0
  44. package/dist/core/values.d.ts +127 -8
  45. package/dist/core/values.js +536 -30
  46. package/dist/core/verdict.d.ts +271 -0
  47. package/dist/core/verdict.js +567 -0
  48. package/dist/core/wasm.d.ts +114 -0
  49. package/dist/core/wasm.js +514 -0
  50. package/dist/generated/sysml_pb.d.ts +1062 -15
  51. package/dist/generated/sysml_pb.js +200 -100
  52. package/dist/node/generate-cli.d.ts +2 -0
  53. package/dist/node/generate-cli.js +10 -0
  54. package/dist/node/generate.d.ts +99 -0
  55. package/dist/node/generate.js +757 -0
  56. package/dist/node/index.d.ts +29 -2
  57. package/dist/node/index.js +211 -10
  58. package/dist/node/service.d.ts +5 -3
  59. package/dist/node/service.js +32 -26
  60. package/dist/node/wasm-source.d.ts +18 -0
  61. package/dist/node/wasm-source.js +103 -0
  62. package/dist/node/wasm-worker.d.ts +1 -0
  63. package/dist/node/wasm-worker.js +10 -0
  64. package/dist/node/wasm.d.ts +17 -0
  65. package/dist/node/wasm.js +63 -0
  66. package/package.json +19 -7
  67. package/release-digests.json +105 -0
@@ -1,12 +1,24 @@
1
1
  // A connection: one Connect client against one service, plus the handshake that
2
2
  // tells the client what that service can do.
3
- import { Code, ConnectError, createClient } from "@connectrpc/connect";
4
- import { ServerInfo } from "./capabilities.js";
5
- import { ClosedConnectionError } from "./errors.js";
6
- import { SysMLService } from "../generated/sysml_pb.js";
7
- import { callRpc, fromHandshakeError } from "./status.js";
8
- import { Model } from "./model.js";
9
- /** A connection to a sysml-grpc service. Close it, or use `await using`. */
3
+ import { Code, ConnectError, createClient, } from "@connectrpc/connect";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { CAPABILITY_APPLY_EDITS, CAPABILITY_BIG_INT_VALUES, CAPABILITY_RATIONAL_VALUES, CAPABILITY_CONVERT, CAPABILITY_DOCUMENT_QUERY, CAPABILITY_ENGINES, CAPABILITY_FEATURE_VALUES, CAPABILITY_INLINE_LANGUAGE, CAPABILITY_MIGRATE, CAPABILITY_PARSE_SOURCES, CAPABILITY_PERFORMER, CAPABILITY_STATE_TRACE, CAPABILITY_QUERY, CAPABILITY_RENDER_DOCUMENT, CAPABILITY_RENDER_DOCUMENT_HTML, CAPABILITY_RENDER_VIEW, CAPABILITY_EXPORT_GRAPHS, CAPABILITY_SCHEDULE, CAPABILITY_SCHEDULE_EXPLORE, CAPABILITY_STRICT_CONFORMANCE, CAPABILITY_VERIFICATION, CAPABILITY_VERIFICATION_QUESTIONS, CAPABILITY_VERIFICATION_ARGUMENTS, capabilityRefusal, mismatchReason, requireCapability, upgradeRemedy, ServerInfo, } from "./capabilities.js";
6
+ import { ClosedConnectionError, ConversionError, ExecutionError, InvalidRequestError, MigrationError, ParseError, StaleServiceError, UnsupportedValueError, } from "./errors.js";
7
+ import { ApplyEditsRequestSchema, ExecuteActionRequestSchema, ExecuteStateRequestSchema, ConvertRequestSchema, ListEnginesRequestSchema, MigrateRequestSchema, ParseSourcesRequestSchema, QueryRequestSchema, RenderDocumentRequestSchema, RenderViewRequestSchema, ExportGraphsRequestSchema, RunAnalysisRequestSchema, RunDocumentQueryRequestSchema, RunSweepRequestSchema, SweepRangeSchema, ValidateInstanceRequestSchema, EvaluateCalcRequestSchema, VerifyConstraintRequestSchema, VerifyRequirementRequestSchema, VerifySatisfactionRequestSchema, FailureReason, SysMLService, } from "../generated/sysml_pb.js";
8
+ import { callRpc, fromHandshakeError, fromRpcError } from "./status.js";
9
+ import { requireString } from "./arguments.js";
10
+ import { decodeDiagnostic, Instance, Model, strictConformanceOf } from "./model.js";
11
+ import { sourceDocuments } from "./sources.js";
12
+ import { Conversion, EXPERIMENTAL_NOTICE, isExperimental, isV1, MIGRATED_NOT_CONVERTED, Migration, MIGRATION_NOTICE, migrationReportOf, pathIsV1, } from "./conversion.js";
13
+ import { buildQuery, elementsOf, } from "./query.js";
14
+ import { bindingHoldsBigInt, bindingHoldsRational, bindingRationalsAsReals, buildBindings, documentEventOf, documentResult, DocumentEvent, } from "./document.js";
15
+ import { renderedViewOf } from "./render-view.js";
16
+ import { graphsOf } from "./graphs.js";
17
+ import { engineInfoOf, standingOf, ENGINE_AUTO, } from "./engines.js";
18
+ import { Exploration, Outcome } from "./exploration.js";
19
+ import { analysisResultOf, decodeVerificationVerdict, raiseFailure, raiseWrongKind, sweepTableOf, validationOf, verdictOf, valuesMap, valueOrError, QUESTION_EVALUATE, CalcResult, } from "./verdict.js";
20
+ import { toValue } from "./values.js";
21
+ import { editErrorOf, editResultOf, EditRequestBuilder, Editor, } from "./edit.js";
10
22
  export class Connection {
11
23
  /**
12
24
  * The generated Connect client. The ergonomic API covers what this version
@@ -25,10 +37,35 @@ export class Connection {
25
37
  this.backend = init.backend;
26
38
  this.timeoutMs = init.timeoutMs;
27
39
  }
28
- /** Opens a connection over `transport` and performs the capability handshake. */
40
+ /**
41
+ * Opens a connection over `transport` and performs the capability handshake.
42
+ * A refused handshake — the service is not the release asked for, or lacks a
43
+ * required capability — releases the backend and is never returned.
44
+ */
29
45
  static async open(init) {
30
46
  const rpc = createClient(SysMLService, init.transport);
31
- const info = await handshake(rpc, init.backend.origin, init.timeoutMs);
47
+ let info;
48
+ try {
49
+ info = await handshake(rpc, init.backend.origin, init.timeoutMs);
50
+ const reason = mismatchReason(info, {
51
+ ...(init.requiredVersion === undefined
52
+ ? {}
53
+ : { version: init.requiredVersion }),
54
+ });
55
+ if (reason !== undefined) {
56
+ throw new StaleServiceError(init.stale?.address ?? init.backend.origin, reason, init.stale?.remedy ??
57
+ "pass a release the service can report, or none", { info });
58
+ }
59
+ for (const capability of init.requiredCapabilities ?? []) {
60
+ requireCapability(info, capability, upgradeRemedy(capability));
61
+ }
62
+ }
63
+ catch (error) {
64
+ // A refused connection is never returned, so nothing else can release
65
+ // the hold the open took on the service.
66
+ await init.backend.release();
67
+ throw error;
68
+ }
32
69
  return new Connection({
33
70
  rpc,
34
71
  info,
@@ -50,6 +87,7 @@ export class Connection {
50
87
  }
51
88
  /** Parses a file the service can read, and returns the model it loaded. */
52
89
  load(path, options = {}) {
90
+ requireString("path", path);
53
91
  return Model.parse(this, { source: { case: "filePath", value: path } }, options);
54
92
  }
55
93
  /** Parses inline source text, and returns the model it loaded. */
@@ -63,15 +101,665 @@ export class Connection {
63
101
  model(hash) {
64
102
  return Model.adopt(this, hash);
65
103
  }
104
+ /** Parses several documents together as one model, one root per document. */
105
+ async parseSources(documents, options = {}) {
106
+ const sources = sourceDocuments(documents);
107
+ requireCapability(this.info, CAPABILITY_PARSE_SOURCES, upgradeRemedy(CAPABILITY_PARSE_SOURCES));
108
+ const capabilities = [CAPABILITY_PARSE_SOURCES];
109
+ if (sources.some((source) => source.language !== undefined)) {
110
+ requireCapability(this.info, CAPABILITY_INLINE_LANGUAGE, upgradeRemedy(CAPABILITY_INLINE_LANGUAGE));
111
+ capabilities.push(CAPABILITY_INLINE_LANGUAGE);
112
+ }
113
+ const strictConformance = strictConformanceOf(options);
114
+ if (strictConformance === true) {
115
+ this.requireStrictConformance();
116
+ capabilities.push(CAPABILITY_STRICT_CONFORMANCE);
117
+ }
118
+ const response = await callRpc(this.rpc.parseSources(create(ParseSourcesRequestSchema, {
119
+ documents: sources.map((source) => source.toPb()),
120
+ strictConformance: strictConformance === true,
121
+ }), this.callOptions()), "file", capabilityRefusal(this.info, capabilities));
122
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
123
+ if (response.error !== "") {
124
+ throw new ParseError(response.error, diagnostics);
125
+ }
126
+ return Model.fromRoots(this, response.modelHash, response.roots, diagnostics, {
127
+ documents: sources.map((source) => source.documentName),
128
+ });
129
+ }
130
+ /**
131
+ * Writes a model out in another of the formats OpenSysML writes. Exactly one
132
+ * of `source.path`, `source.content` and `source.modelHash` names the source.
133
+ */
134
+ async convert(toFormat, source, options = {}) {
135
+ const given = ["path", "content", "modelHash"].filter((name) => source[name] !== undefined);
136
+ if (given.length !== 1) {
137
+ throw new RangeError("provide exactly one of path, content or modelHash; got " +
138
+ (given.length > 0 ? given.join(", ") : "none"));
139
+ }
140
+ const fromFormat = options.fromFormat ?? "";
141
+ if (isV1(fromFormat) ||
142
+ (fromFormat === "" && "path" in source && pathIsV1(source.path))) {
143
+ const name = "path" in source ? source.path : "the source";
144
+ throw new InvalidRequestError(`${name} ${MIGRATED_NOT_CONVERTED}; call migrate() with the same source`, {
145
+ code: "INVALID_ARGUMENT",
146
+ });
147
+ }
148
+ requireCapability(this.info, CAPABILITY_CONVERT, upgradeRemedy(CAPABILITY_CONVERT));
149
+ const request = create(ConvertRequestSchema, {
150
+ toFormat,
151
+ fromFormat: options.fromFormat ?? "",
152
+ tolerateSyntaxErrors: options.tolerateSyntaxErrors === true,
153
+ });
154
+ if ("path" in source) {
155
+ request.source = { case: "filePath", value: source.path };
156
+ }
157
+ else if ("content" in source) {
158
+ request.source = { case: "content", value: source.content };
159
+ }
160
+ else {
161
+ request.source = { case: "modelHash", value: source.modelHash };
162
+ }
163
+ const response = await callRpc(this.rpc.convert(request, this.callOptions()), "path" in source ? "file" : "model", capabilityRefusal(this.info, [CAPABILITY_CONVERT]));
164
+ // Judged from the response, so an inferred format counts and a service too
165
+ // old to mark the conversion is still read as experimental.
166
+ const experimental = response.experimental ||
167
+ isExperimental(response.fromFormat, response.toFormat);
168
+ let notice = response.experimentalNotice;
169
+ if (notice === "" && experimental) {
170
+ notice = EXPERIMENTAL_NOTICE;
171
+ }
172
+ if (experimental) {
173
+ // Warned before the error is raised: a refusal is the mapping's
174
+ // experimental behavior, not a reason to say nothing about it.
175
+ this.warn(notice, "ExperimentalFeatureWarning");
176
+ }
177
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
178
+ if (response.error !== "") {
179
+ throw new ConversionError(response.error, diagnostics);
180
+ }
181
+ return new Conversion({
182
+ content: response.content,
183
+ fromFormat: response.fromFormat,
184
+ toFormat: response.toFormat,
185
+ diagnostics,
186
+ experimental,
187
+ experimentalNotice: notice,
188
+ });
189
+ }
190
+ /**
191
+ * Migrates a SysML v1 model — a Cameo/MagicDraw `.mdzip`, a UML XMI `.xmi` or
192
+ * an Eclipse UML2 `.uml` export — to one of the formats OpenSysML writes. A
193
+ * migration is ledgered, not lossless: every v1 element lands in the
194
+ * `Migration.report` as mapped, approximated, unmapped or skipped. Exactly one
195
+ * of `source.path` and `source.content` names the source; inline content is
196
+ * the file's bytes and needs `fromFormat` to say which form they are.
197
+ */
198
+ async migrate(toFormat, source, options = {}) {
199
+ const given = ["path", "content"].filter((name) => source[name] !== undefined);
200
+ if (given.length !== 1) {
201
+ throw new RangeError("provide exactly one of path or content; got " +
202
+ (given.length > 0 ? given.join(", ") : "none"));
203
+ }
204
+ if (options.layoutPath !== undefined &&
205
+ options.layoutContent !== undefined) {
206
+ throw new RangeError("provide at most one of layoutPath and layoutContent");
207
+ }
208
+ const fromFormat = options.fromFormat ?? "";
209
+ if (fromFormat !== "" && !isV1(fromFormat)) {
210
+ const name = "path" in source ? source.path : "the source";
211
+ throw new InvalidRequestError(`${name} is ${fromFormat} input, which is converted, not migrated: only a SysML v1 model ` +
212
+ "(xmi, uml or mdzip) is migrated; call convert() with the same source", { code: "INVALID_ARGUMENT" });
213
+ }
214
+ requireCapability(this.info, CAPABILITY_MIGRATE, upgradeRemedy(CAPABILITY_MIGRATE));
215
+ // Remembered absolute before the call, against the working directory of
216
+ // the moment the source was named rather than of the moment it answers.
217
+ const sourcePath = "path" in source
218
+ ? (this.backend.resolvePath?.(source.path) ?? source.path)
219
+ : "";
220
+ const request = create(MigrateRequestSchema, {
221
+ toFormat,
222
+ fromFormat,
223
+ report: options.report === true,
224
+ results: options.results === true,
225
+ imageBaseUrl: options.imageBaseUrl ?? "",
226
+ strict: options.strict === true,
227
+ });
228
+ if ("path" in source) {
229
+ request.source = { case: "filePath", value: source.path };
230
+ }
231
+ else {
232
+ request.source = { case: "content", value: source.content };
233
+ }
234
+ if (options.layoutPath !== undefined) {
235
+ request.layout = { case: "layoutPath", value: options.layoutPath };
236
+ }
237
+ else if (options.layoutContent !== undefined) {
238
+ request.layout = { case: "layoutContent", value: options.layoutContent };
239
+ }
240
+ const response = await callRpc(this.rpc.migrate(request, this.callOptions()), "path" in source ? "file" : "model", capabilityRefusal(this.info, [CAPABILITY_MIGRATE]));
241
+ const notice = response.experimentalNotice !== ""
242
+ ? response.experimentalNotice
243
+ : MIGRATION_NOTICE;
244
+ // Warned before the error is raised: a refusal is the mapping's
245
+ // experimental behavior, not a reason to say nothing about it.
246
+ this.warn(notice, "ExperimentalFeatureWarning");
247
+ if (response.error !== "") {
248
+ throw new MigrationError(response.error);
249
+ }
250
+ return new Migration({
251
+ content: response.content,
252
+ fromFormat: response.fromFormat,
253
+ toFormat: response.toFormat,
254
+ report: migrationReportOf(response.report),
255
+ results: response.results,
256
+ files: new Map(response.files.map((file) => [file.path, file.content])),
257
+ sourcePath,
258
+ experimentalNotice: notice,
259
+ });
260
+ }
261
+ /** Runs a SysML v2 API & Services Query over a loaded model. */
262
+ async query(modelHash, options = {}) {
263
+ requireCapability(this.info, CAPABILITY_QUERY, upgradeRemedy(CAPABILITY_QUERY));
264
+ const response = await callRpc(this.rpc.query(create(QueryRequestSchema, {
265
+ modelHash,
266
+ oslcQuery: options.oslc ?? "",
267
+ ...queryField(options),
268
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [CAPABILITY_QUERY]));
269
+ return elementsOf(response);
270
+ }
271
+ /** Runs a named document query and answers its typed rows. */
272
+ async runDocumentQuery(modelHash, queryId, bindings) {
273
+ requireCapability(this.info, CAPABILITY_DOCUMENT_QUERY, upgradeRemedy(CAPABILITY_DOCUMENT_QUERY));
274
+ const wire = buildBindings(bindings);
275
+ if (wire.some(bindingHoldsBigInt)) {
276
+ requireCapability(this.info, CAPABILITY_BIG_INT_VALUES, upgradeRemedy(CAPABILITY_BIG_INT_VALUES));
277
+ }
278
+ if (!this.info.has(CAPABILITY_RATIONAL_VALUES)) {
279
+ wire.forEach(bindingRationalsAsReals);
280
+ }
281
+ if (wire.some(bindingHoldsRational)) {
282
+ requireCapability(this.info, CAPABILITY_RATIONAL_VALUES, upgradeRemedy(CAPABILITY_RATIONAL_VALUES));
283
+ }
284
+ const response = await callRpc(this.rpc.runDocumentQuery(create(RunDocumentQueryRequestSchema, {
285
+ modelHash,
286
+ queryId,
287
+ bindings: wire,
288
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [CAPABILITY_DOCUMENT_QUERY]));
289
+ return documentResult(response);
290
+ }
291
+ /** Applies source-preserving edits to a loaded model. Operations are the
292
+ * tuple form the {@link Editor} builds, or wire `EditOperation` messages,
293
+ * which pass through as written. */
294
+ async applyEdits(modelHash, operations, options = {}) {
295
+ requireCapability(this.info, CAPABILITY_APPLY_EDITS, upgradeRemedy(CAPABILITY_APPLY_EDITS));
296
+ const { operations: built, capabilities } = new EditRequestBuilder(this.info).build(operations);
297
+ const response = await callRpc(this.rpc.applyEdits(create(ApplyEditsRequestSchema, {
298
+ modelHash,
299
+ operations: built,
300
+ acceptDocuments: options.acceptDocuments ?? true,
301
+ document: options.document ?? "",
302
+ }), this.callOptions()), "model", capabilityRefusal(this.info, capabilities));
303
+ const refusal = editErrorOf(response);
304
+ if (refusal !== undefined) {
305
+ throw refusal;
306
+ }
307
+ return editResultOf(response);
308
+ }
309
+ /** Starts an edit of a loaded model, applied with {@link Editor.apply}. */
310
+ edit(modelHash) {
311
+ return new Editor(modelHash, (hash, operations) => this.applyEdits(hash, operations));
312
+ }
313
+ /** Renders a named document to Markdown or HTML. */
314
+ async renderDocument(modelHash, documentId, options = {}) {
315
+ const form = options.form ?? "markdown";
316
+ if (form !== "markdown" && form !== "html") {
317
+ throw new RangeError("form must be 'markdown' or 'html'");
318
+ }
319
+ const capabilities = [CAPABILITY_RENDER_DOCUMENT];
320
+ if (form === "html") {
321
+ capabilities.push(CAPABILITY_RENDER_DOCUMENT_HTML);
322
+ }
323
+ for (const capability of capabilities) {
324
+ requireCapability(this.info, capability, upgradeRemedy(capability));
325
+ }
326
+ const response = await callRpc(this.rpc.renderDocument(create(RenderDocumentRequestSchema, {
327
+ modelHash,
328
+ documentId,
329
+ form: form === "markdown" ? "" : form,
330
+ }), this.callOptions()), "model", capabilityRefusal(this.info, capabilities));
331
+ return form === "html" ? response.html : response.markdown;
332
+ }
333
+ /** Renders a named view or targeted pseudo-view as diagram data. */
334
+ async renderView(modelHash, viewName, options = {}) {
335
+ const ports = options.ports ?? "minimal";
336
+ if (ports !== "minimal" && ports !== "full") {
337
+ throw new RangeError("ports must be 'minimal' or 'full'");
338
+ }
339
+ const capabilities = [CAPABILITY_RENDER_VIEW];
340
+ for (const capability of capabilities) {
341
+ requireCapability(this.info, capability, upgradeRemedy(capability));
342
+ }
343
+ const response = await callRpc(this.rpc.renderView(create(RenderViewRequestSchema, {
344
+ modelHash,
345
+ view: viewName,
346
+ ports: ports === "minimal" ? "" : ports,
347
+ }), this.callOptions()), "model", capabilityRefusal(this.info, capabilities));
348
+ return renderedViewOf(response);
349
+ }
350
+ /**
351
+ * Exports the lowered graph of an action or state machine, and of every
352
+ * behavior it performs, as the canonical `graphs:1` JSON an external
353
+ * analysis engine is sent.
354
+ */
355
+ async exportGraphs(modelHash, subject) {
356
+ const capabilities = [CAPABILITY_EXPORT_GRAPHS];
357
+ for (const capability of capabilities) {
358
+ requireCapability(this.info, capability, upgradeRemedy(capability));
359
+ }
360
+ const response = await callRpc(this.rpc.exportGraphs(create(ExportGraphsRequestSchema, { modelHash, subject }), this.callOptions()), "model", capabilityRefusal(this.info, capabilities));
361
+ return graphsOf(response);
362
+ }
363
+ /** Executes an action definition. */
364
+ async executeAction(modelHash, actionSymbolId, options = {}) {
365
+ refuseExploring(options.schedule, "exploreAction");
366
+ const response = await this.sendExecuteAction(modelHash, actionSymbolId, options);
367
+ if (response.error !== "") {
368
+ throw new ExecutionError(response.error, "unspecified", response.diagnostics.map(decodeDiagnostic));
369
+ }
370
+ return {
371
+ outputs: valuesMap(Object.entries(response.outputs)),
372
+ performer: valuesMap(Object.entries(response.performerAttributes)),
373
+ finalTime: response.finalTime,
374
+ };
375
+ }
376
+ /** Runs an action once per valid order of its choice points, within a budget. */
377
+ async exploreAction(modelHash, actionSymbolId, options = {}) {
378
+ requireExploring(options.schedule ?? "explore");
379
+ const response = await this.sendExecuteAction(modelHash, actionSymbolId, {
380
+ ...options,
381
+ schedule: options.schedule ?? "explore",
382
+ });
383
+ return this.explorationOf(response);
384
+ }
385
+ async sendExecuteAction(modelHash, actionSymbolId, options) {
386
+ const inputs = {};
387
+ for (const [name, value] of Object.entries(options.inputs ?? {})) {
388
+ inputs[name] = toValue(value, this.info);
389
+ }
390
+ const capabilities = this.runCapabilities(options.schedule, options.performer);
391
+ const response = await callRpc(this.rpc.executeAction(create(ExecuteActionRequestSchema, {
392
+ modelHash,
393
+ actionSymbolId,
394
+ inputs,
395
+ schedule: options.schedule ?? "",
396
+ performerSymbolId: options.performer ?? "",
397
+ }), this.callOptions()), "model", capabilityRefusal(this.info, capabilities));
398
+ return response;
399
+ }
400
+ /** Executes a state machine; a failed traced run carries its partial trace on ExecutionError. */
401
+ async executeState(modelHash, stateMachineSymbolId, options = {}) {
402
+ refuseExploring(options.schedule, "exploreState");
403
+ const response = await this.sendExecuteState(modelHash, stateMachineSymbolId, options);
404
+ if (response.error !== "") {
405
+ throw new ExecutionError(response.error, "unspecified", response.diagnostics.map(decodeDiagnostic), response.trace.map(documentEventOf), response.traceDropped);
406
+ }
407
+ return {
408
+ statesVisited: [...response.statesVisited],
409
+ finalContext: valuesMap(Object.entries(response.finalContext)),
410
+ finalTime: response.finalTime,
411
+ trace: response.trace.map(documentEventOf),
412
+ traceDropped: response.traceDropped,
413
+ };
414
+ }
415
+ /** Runs a state machine over the events once per valid order of its choice points. */
416
+ async exploreState(modelHash, stateMachineSymbolId, options = {}) {
417
+ requireExploring(options.schedule ?? "explore");
418
+ const response = await this.sendExecuteState(modelHash, stateMachineSymbolId, {
419
+ ...options,
420
+ schedule: options.schedule ?? "explore",
421
+ });
422
+ return this.explorationOf(response);
423
+ }
424
+ async sendExecuteState(modelHash, stateMachineSymbolId, options) {
425
+ const capabilities = this.runCapabilities(options.schedule, options.performer, options.trace);
426
+ const response = await callRpc(this.rpc.executeState(create(ExecuteStateRequestSchema, {
427
+ modelHash,
428
+ stateMachineSymbolId,
429
+ events: [...(options.events ?? [])],
430
+ schedule: options.schedule ?? "",
431
+ performerSymbolId: options.performer ?? "",
432
+ trace: options.trace ?? false,
433
+ }), this.callOptions()), "model", capabilityRefusal(this.info, capabilities));
434
+ return response;
435
+ }
436
+ explorationOf(response) {
437
+ if (response.error !== "") {
438
+ const reason = "failureReason" in response
439
+ ? response.failureReason
440
+ : FailureReason.UNSPECIFIED;
441
+ raiseFailure(response.error, reason, response.diagnostics.map(decodeDiagnostic));
442
+ }
443
+ const status = response.exploration;
444
+ return new Exploration({
445
+ outcomes: response.outcomes.map((pb) => outcomeOf(pb)),
446
+ complete: status?.complete ?? false,
447
+ runs: status?.runs ?? 0,
448
+ budgetsHit: status?.budgetsHit ?? [],
449
+ runsBudget: status?.runsBudget ?? 0,
450
+ depthBudget: status?.depthBudget ?? 0,
451
+ probabilitiesLowerBound: status?.probabilitiesLowerBound ?? false,
452
+ });
453
+ }
454
+ /** Lists the analysis engines the service answers with, as `sysml -engines` does. */
455
+ async listEngines() {
456
+ this.requireEngines();
457
+ const response = await callRpc(this.rpc.listEngines(create(ListEnginesRequestSchema, {}), this.callOptions()), "model", capabilityRefusal(this.info, [CAPABILITY_ENGINES]));
458
+ return response.engines.map(engineInfoOf);
459
+ }
460
+ /** Asks whether a constraint holds. */
461
+ async verifyConstraint(modelHash, symbolId, options = {}) {
462
+ this.requireVerification();
463
+ this.requireEngine(options.engine);
464
+ this.requireQuestion(options.question);
465
+ this.requireArguments(options);
466
+ const namedArguments = {};
467
+ for (const [name, arg] of Object.entries(options.namedArguments ?? {})) {
468
+ namedArguments[name] = toValue(arg, this.info);
469
+ }
470
+ const response = await callRpc(this.rpc.verifyConstraint(create(VerifyConstraintRequestSchema, {
471
+ modelHash,
472
+ symbolId,
473
+ subjectSymbolId: options.subject ?? "",
474
+ engine: engineField(options.engine),
475
+ question: questionField(options.question),
476
+ arguments: (options.arguments ?? []).map((arg) => toValue(arg, this.info)),
477
+ namedArguments,
478
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [
479
+ CAPABILITY_VERIFICATION,
480
+ ...engineCapabilities(options.engine),
481
+ ...questionCapabilities(options.question),
482
+ ...argumentCapabilities(options),
483
+ ]));
484
+ return this.verdictOf(response);
485
+ }
486
+ /** Asks whether a requirement is satisfied. */
487
+ async verifyRequirement(modelHash, symbolId, options = {}) {
488
+ this.requireVerification();
489
+ this.requireEngine(options.engine);
490
+ this.requireQuestion(options.question);
491
+ this.requireArguments(options);
492
+ const namedArguments = {};
493
+ for (const [name, arg] of Object.entries(options.namedArguments ?? {})) {
494
+ namedArguments[name] = toValue(arg, this.info);
495
+ }
496
+ const response = await callRpc(this.rpc.verifyRequirement(create(VerifyRequirementRequestSchema, {
497
+ modelHash,
498
+ symbolId,
499
+ subjectSymbolId: options.subject ?? "",
500
+ engine: engineField(options.engine),
501
+ question: questionField(options.question),
502
+ arguments: (options.arguments ?? []).map((arg) => toValue(arg, this.info)),
503
+ namedArguments,
504
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [
505
+ CAPABILITY_VERIFICATION,
506
+ ...engineCapabilities(options.engine),
507
+ ...questionCapabilities(options.question),
508
+ ...argumentCapabilities(options),
509
+ ]));
510
+ return this.verdictOf(response);
511
+ }
512
+ /** Asks whether the model's satisfaction assertions hold. */
513
+ async verifySatisfaction(modelHash, options = {}) {
514
+ this.requireVerification();
515
+ this.requireEngine(options.engine);
516
+ this.requireQuestion(options.question);
517
+ const response = await callRpc(this.rpc.verifySatisfaction(create(VerifySatisfactionRequestSchema, {
518
+ modelHash,
519
+ symbolId: options.symbolId ?? "",
520
+ engine: engineField(options.engine),
521
+ question: questionField(options.question),
522
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [
523
+ CAPABILITY_VERIFICATION,
524
+ ...engineCapabilities(options.engine),
525
+ ...questionCapabilities(options.question),
526
+ ]));
527
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
528
+ if (response.error !== "") {
529
+ raiseFailure(response.error, response.failureReason, diagnostics);
530
+ }
531
+ for (const pbVerdict of response.verdicts) {
532
+ raiseWrongKind(pbVerdict, diagnostics);
533
+ }
534
+ const instances = this.instancesOf(response.instances);
535
+ const verifications = response.verificationVerdicts.map(decodeVerificationVerdict);
536
+ return response.verdicts.map((pbVerdict) => verdictOf(pbVerdict, instances, diagnostics, verifications));
537
+ }
538
+ /** Checks every assertion about an object of one of this model's parts. */
539
+ async validateInstance(modelHash, symbolId, options = {}) {
540
+ this.requireVerification();
541
+ this.requireEngine(options.engine);
542
+ const response = await callRpc(this.rpc.validateInstance(create(ValidateInstanceRequestSchema, {
543
+ modelHash,
544
+ symbolId,
545
+ engine: engineField(options.engine),
546
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [
547
+ CAPABILITY_VERIFICATION,
548
+ ...engineCapabilities(options.engine),
549
+ ]));
550
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
551
+ if (response.error !== "") {
552
+ raiseFailure(response.error, response.failureReason, diagnostics);
553
+ }
554
+ return validationOf(response, this.instancesOf(response.instances), diagnostics);
555
+ }
556
+ /** Invokes a calculation. */
557
+ async calc(modelHash, symbolId, options = {}) {
558
+ this.requireVerification();
559
+ this.requireEngine(options.engine);
560
+ const response = await callRpc(this.rpc.evaluateCalc(create(EvaluateCalcRequestSchema, {
561
+ modelHash,
562
+ symbolId,
563
+ arguments: (options.arguments ?? []).map((arg) => toValue(arg, this.info)),
564
+ engine: engineField(options.engine),
565
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [
566
+ CAPABILITY_VERIFICATION,
567
+ ...engineCapabilities(options.engine),
568
+ ]));
569
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
570
+ if (response.error !== "") {
571
+ raiseFailure(response.error, response.failureReason, diagnostics);
572
+ }
573
+ const outputs = new Map();
574
+ for (const output of response.outputs) {
575
+ outputs.set(output.name, valueOrError(output.value));
576
+ }
577
+ const result = outputs.size === 0 && response.result !== undefined
578
+ ? valueOrError(response.result)
579
+ : undefined;
580
+ return new CalcResult({
581
+ ...(result === undefined ? {} : { value: result }),
582
+ outputs,
583
+ diagnostics,
584
+ standing: standingOf(response),
585
+ });
586
+ }
587
+ /** Runs an analysis case. */
588
+ async runAnalysis(modelHash, symbolId, options = {}) {
589
+ refuseExploring(options.schedule, "exploreAnalysis");
590
+ if (options.engine === "explore") {
591
+ throw new RangeError("engine 'explore' answers with every outcome, not one run's result: use exploreAnalysis");
592
+ }
593
+ const response = await this.sendRunAnalysis(modelHash, symbolId, options);
594
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
595
+ // A request refused before the run, or a failure leaving nothing to
596
+ // report, has no partial result.
597
+ if (response.error !== "" &&
598
+ response.outputs.length === 0 &&
599
+ response.verdicts.length === 0 &&
600
+ response.evaluations.length === 0 &&
601
+ response.instances.length === 0) {
602
+ raiseFailure(response.error, response.failureReason, diagnostics);
603
+ }
604
+ return analysisResultOf(response, this.instancesOf(response.instances), diagnostics);
605
+ }
606
+ /** Runs an analysis case once per valid order of its choice points. */
607
+ async exploreAnalysis(modelHash, symbolId, options = {}) {
608
+ requireExploring(options.schedule ?? "explore");
609
+ const response = await this.sendRunAnalysis(modelHash, symbolId, {
610
+ ...options,
611
+ schedule: options.schedule ?? "explore",
612
+ });
613
+ return this.explorationOf(response);
614
+ }
615
+ async sendRunAnalysis(modelHash, symbolId, options) {
616
+ this.requireVerification();
617
+ this.requireSchedule(options.schedule);
618
+ this.requireEngine(options.engine);
619
+ const namedArguments = {};
620
+ for (const [name, arg] of Object.entries(options.namedArguments ?? {})) {
621
+ namedArguments[name] = toValue(arg, this.info);
622
+ }
623
+ const response = await callRpc(this.rpc.runAnalysis(create(RunAnalysisRequestSchema, {
624
+ modelHash,
625
+ symbolId,
626
+ subjectSymbolId: options.subject ?? "",
627
+ arguments: (options.arguments ?? []).map((arg) => toValue(arg, this.info)),
628
+ namedArguments,
629
+ schedule: options.schedule ?? "",
630
+ engine: engineField(options.engine),
631
+ }), this.callOptions()), "model", capabilityRefusal(this.info, [
632
+ CAPABILITY_VERIFICATION,
633
+ ...scheduleCapabilities(options.schedule),
634
+ ...engineCapabilities(options.engine),
635
+ ]));
636
+ return response;
637
+ }
638
+ /** Runs an analysis case or calc once per row of a parameter sweep. */
639
+ async runSweep(modelHash, symbolId, ranges, options = {}) {
640
+ this.requireVerification();
641
+ this.requireEngine(options.engine);
642
+ const namedArguments = {};
643
+ for (const [name, arg] of Object.entries(options.namedArguments ?? {})) {
644
+ namedArguments[name] = toValue(arg, this.info);
645
+ }
646
+ const request = create(RunSweepRequestSchema, {
647
+ modelHash,
648
+ symbolId,
649
+ subjectSymbolId: options.subject ?? "",
650
+ arguments: (options.arguments ?? []).map((arg) => toValue(arg, this.info)),
651
+ namedArguments,
652
+ samples: BigInt(options.samples ?? 0),
653
+ seed: BigInt(options.seed ?? 0),
654
+ engine: engineField(options.engine),
655
+ });
656
+ for (const [name, bounds] of Object.entries(ranges)) {
657
+ request.ranges.push(this.sweepRange(name, bounds));
658
+ }
659
+ const response = await callRpc(this.rpc.runSweep(request, this.callOptions()), "model", capabilityRefusal(this.info, [
660
+ CAPABILITY_VERIFICATION,
661
+ ...engineCapabilities(options.engine),
662
+ ]));
663
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
664
+ if (response.error !== "") {
665
+ raiseFailure(response.error, response.failureReason, diagnostics);
666
+ }
667
+ return sweepTableOf(response, this.instancesOf(response.instances), diagnostics);
668
+ }
669
+ sweepRange(name, bounds) {
670
+ if (bounds.length !== 2 && bounds.length !== 3) {
671
+ throw new InvalidRequestError(`range of ${name} takes (from, to) or (from, to, step), not ${bounds.length} value(s)`);
672
+ }
673
+ return create(SweepRangeSchema, {
674
+ parameter: name,
675
+ start: toValue(bounds[0], this.info),
676
+ end: toValue(bounds[1], this.info),
677
+ ...(bounds.length === 3 ? { step: toValue(bounds[2], this.info) } : {}),
678
+ });
679
+ }
680
+ verdictOf(response) {
681
+ const diagnostics = response.diagnostics.map(decodeDiagnostic);
682
+ if (response.error !== "") {
683
+ throw new ExecutionError(response.error, "unspecified", diagnostics);
684
+ }
685
+ const pbVerdict = response.verdict;
686
+ if (pbVerdict === undefined) {
687
+ throw new ExecutionError("the service answered the verification without a verdict");
688
+ }
689
+ raiseWrongKind(pbVerdict, diagnostics);
690
+ return verdictOf(pbVerdict, this.instancesOf(response.instances), diagnostics, (response.verificationVerdicts ?? []).map(decodeVerificationVerdict), true);
691
+ }
692
+ instancesOf(pbInstances) {
693
+ if (pbInstances.length > 0) {
694
+ requireCapability(this.info, CAPABILITY_FEATURE_VALUES, upgradeRemedy(CAPABILITY_FEATURE_VALUES));
695
+ }
696
+ return pbInstances.map((pb) => new Instance(pb));
697
+ }
698
+ requireVerification() {
699
+ requireCapability(this.info, CAPABILITY_VERIFICATION, upgradeRemedy(CAPABILITY_VERIFICATION));
700
+ }
701
+ requireEngines() {
702
+ requireCapability(this.info, CAPABILITY_ENGINES, upgradeRemedy(CAPABILITY_ENGINES));
703
+ }
704
+ requireEngine(engine) {
705
+ for (const capability of engineCapabilities(engine)) {
706
+ requireCapability(this.info, capability, upgradeRemedy(capability));
707
+ }
708
+ }
709
+ requireArguments(options) {
710
+ for (const capability of argumentCapabilities(options)) {
711
+ requireCapability(this.info, capability, upgradeRemedy(capability));
712
+ }
713
+ }
714
+ requireQuestion(question) {
715
+ for (const capability of questionCapabilities(question)) {
716
+ requireCapability(this.info, capability, upgradeRemedy(capability));
717
+ }
718
+ }
719
+ requireSchedule(schedule) {
720
+ for (const capability of scheduleCapabilities(schedule)) {
721
+ requireCapability(this.info, capability, upgradeRemedy(capability));
722
+ }
723
+ }
724
+ requireStrictConformance() {
725
+ requireCapability(this.info, CAPABILITY_STRICT_CONFORMANCE, upgradeRemedy(CAPABILITY_STRICT_CONFORMANCE));
726
+ }
727
+ runCapabilities(schedule, performer, trace = false) {
728
+ const capabilities = scheduleCapabilities(schedule);
729
+ if (performer !== undefined && performer !== "") {
730
+ capabilities.push(CAPABILITY_PERFORMER);
731
+ }
732
+ if (trace) {
733
+ capabilities.push(CAPABILITY_STATE_TRACE);
734
+ }
735
+ for (const capability of capabilities) {
736
+ requireCapability(this.info, capability, upgradeRemedy(capability));
737
+ }
738
+ return capabilities;
739
+ }
740
+ warn(message, type) {
741
+ this.backend.warn?.(message, type);
742
+ }
66
743
  /** Asks the service what it is and what it can do, again. */
67
744
  async serverInfo() {
68
- const response = await callRpc(this.rpc.getServerInfo({}, this.callOptions()));
69
- return new ServerInfo({
70
- version: response.version,
71
- capabilities: response.capabilities,
72
- answered: true,
73
- origin: this.backend.origin,
74
- });
745
+ try {
746
+ const response = await this.rpc.getServerInfo({}, this.callOptions());
747
+ return new ServerInfo({
748
+ version: response.version,
749
+ capabilities: response.capabilities,
750
+ answered: true,
751
+ origin: this.backend.origin,
752
+ });
753
+ }
754
+ catch (error) {
755
+ const connectError = ConnectError.from(error);
756
+ // A service too old to answer is reported the same way the handshake
757
+ // reports it: no version, no capabilities, unanswered.
758
+ if (connectError.code === Code.Unimplemented) {
759
+ return new ServerInfo({ version: "", capabilities: [], answered: false, origin: this.backend.origin });
760
+ }
761
+ throw fromRpcError(error);
762
+ }
75
763
  }
76
764
  /**
77
765
  * Releases this connection. A private child loses a reference and stops when
@@ -103,8 +791,113 @@ async function handshake(rpc, origin, timeoutMs) {
103
791
  const connectError = ConnectError.from(error);
104
792
  // A service too old to answer the handshake is still usable; anything else is not.
105
793
  if (connectError.code === Code.Unimplemented) {
106
- return new ServerInfo({ version: "", capabilities: [], answered: false, origin });
794
+ return new ServerInfo({
795
+ version: "",
796
+ capabilities: [],
797
+ answered: false,
798
+ origin,
799
+ });
107
800
  }
108
801
  throw fromHandshakeError(connectError, origin);
109
802
  }
110
803
  }
804
+ // Whether a schedule spelling names the exploring policy, options or not.
805
+ function explores(schedule) {
806
+ return (schedule !== undefined &&
807
+ schedule !== "" &&
808
+ (schedule === "explore" || schedule.startsWith("explore:")));
809
+ }
810
+ // The engine field as sent: empty for auto, which every service reads as such.
811
+ function engineField(engine) {
812
+ return engine === undefined || engine === "" || engine === ENGINE_AUTO
813
+ ? ""
814
+ : engine;
815
+ }
816
+ // The question field as sent: empty for evaluate, which every service reads as such.
817
+ function questionField(question) {
818
+ return question === undefined ||
819
+ question === "" ||
820
+ question === QUESTION_EVALUATE
821
+ ? ""
822
+ : question;
823
+ }
824
+ // The capabilities a schedule spelling needs of the service: none for the default.
825
+ function scheduleCapabilities(schedule) {
826
+ const capabilities = [];
827
+ if (schedule !== undefined && schedule !== "") {
828
+ capabilities.push(CAPABILITY_SCHEDULE);
829
+ }
830
+ if (explores(schedule)) {
831
+ capabilities.push(CAPABILITY_SCHEDULE_EXPLORE);
832
+ }
833
+ return capabilities;
834
+ }
835
+ // The capabilities an engine selection needs of the service: none for auto.
836
+ function engineCapabilities(engine) {
837
+ const capabilities = [];
838
+ if (engineField(engine) !== "") {
839
+ capabilities.push(CAPABILITY_ENGINES);
840
+ }
841
+ if (engine === "explore") {
842
+ capabilities.push(CAPABILITY_SCHEDULE_EXPLORE);
843
+ }
844
+ return capabilities;
845
+ }
846
+ // The capabilities a question needs of the service: none for evaluate.
847
+ function argumentCapabilities(options) {
848
+ return (options.arguments?.length ?? 0) > 0 ||
849
+ Object.keys(options.namedArguments ?? {}).length > 0
850
+ ? [CAPABILITY_VERIFICATION_ARGUMENTS]
851
+ : [];
852
+ }
853
+ function questionCapabilities(question) {
854
+ return questionField(question) !== ""
855
+ ? [CAPABILITY_VERIFICATION_QUESTIONS]
856
+ : [];
857
+ }
858
+ // Refuse an exploring schedule on a method answering one run's result.
859
+ function refuseExploring(schedule, method) {
860
+ if (explores(schedule)) {
861
+ throw new RangeError(`schedule ${JSON.stringify(schedule)} answers with every outcome, not one run's ` +
862
+ `result: use ${method}`);
863
+ }
864
+ }
865
+ // Refuse a schedule that would answer one run's result on a method reading outcomes.
866
+ function requireExploring(schedule) {
867
+ if (!explores(schedule)) {
868
+ throw new RangeError(`schedule ${JSON.stringify(schedule)} answers one run's result, not every outcome: ` +
869
+ `spell it 'explore' or 'explore:runs=<n>,depth=<d>'`);
870
+ }
871
+ }
872
+ /** One wire outcome read, an undecodable value kept as its error. */
873
+ function outcomeOf(pb) {
874
+ const outputs = new Map();
875
+ for (const [name, value] of Object.entries(pb.outputs)) {
876
+ outputs.set(name, valueOrError(value));
877
+ }
878
+ return new Outcome({
879
+ outputs,
880
+ finalState: pb.finalState,
881
+ statesVisited: pb.statesVisited,
882
+ error: pb.error,
883
+ linearizations: pb.linearizations,
884
+ probability: pb.probability,
885
+ witness: pb.witness,
886
+ diagnostics: pb.diagnostics.map(decodeDiagnostic),
887
+ });
888
+ }
889
+ /**
890
+ * The Query a request sends. An OSLC-only request sends just oslcQuery (the
891
+ * two are mutually exclusive); every other request sends a Query, empty when
892
+ * nothing was asked for, which answers every element. An empty oslc is no
893
+ * OSLC at all, so a structured ask beside it still sends its Query.
894
+ */
895
+ function queryField(options) {
896
+ if (options.query !== undefined) {
897
+ return { query: options.query };
898
+ }
899
+ if (options.oslc === undefined || options.oslc === "") {
900
+ return { query: buildQuery(options) };
901
+ }
902
+ return {};
903
+ }