@suss/ir-core 0.8.0 → 0.9.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 CHANGED
@@ -31,6 +31,49 @@ The boundary key uses only the subject, so both forms end up in the same bucket
31
31
 
32
32
  Schemas are the single source of truth (`@suss/ir-core/schemas`); the types are derived from them. The recursive `TypeShape` is a hand-written named export so consuming packages reference it by name across the package boundary rather than inlining the recursion.
33
33
 
34
+ ## Names with a hole in them
35
+
36
+ A deployed resource is often called something built at deploy time. A template writes `!Sub "${StageName}-orders-v1"` and the code writes `` `${stage}-orders-v1` ``, so neither side states a string, both agree about the fixed text, and each spells the parameter its own way. A name is written here as fixed text with `{}` holes, and `namesAgree` says whether two of them are the same name.
37
+
38
+ A hole stops at the separator between it and what comes next. No separator works for every project, so the rule does not name one, it takes the separator from the pattern: when the fixed text after a hole starts with a character that is not a letter or a digit, that character divides the deploy-time value from the rest of the name, and the value may not contain it. So `{env}-publications-v1` covers `prod-publications-v1`, and it does not cover `prod-creator-publications-v1`, which the hole could only reach by swallowing a `-`.
39
+
40
+ A greedy hole was the earlier rule, and a module declaring both of those tables had one storage access pair with each of them. The table the code never touches keys on something else, so the run reported a selector mismatch on a boundary that code never reaches.
41
+
42
+ A hole at the end of a name, and a hole whose next character is a letter or a digit, has no separator to stop at, and it still covers anything. That costs a pair when the value has the separator inside it: `{region}-orders` does not cover `us-east-1-orders`, though `orders-{region}` still covers `orders-us-east-1`. Missing a pair is the better failure of the two, since a wrong match reports findings about a store the code never touches.
43
+
44
+ One name can still be covered by two patterns. A hole at the end covers anything, so `orders-{suffix}` and `orders-blue-{suffix}` both cover `orders-blue-v1`. Choosing between them belongs to whichever pass picks a provider for an access, and `fixedTextLength` is what such a pass ranks by: the pattern that states more fixed text is the more specific one, and two patterns that state the same amount settle nothing between them.
45
+
46
+ ## Words OpenTelemetry already has
47
+
48
+ A summary says what a unit can reach. A trace says what it did reach. Comparing the two is the point, and it only works if both sides spell a boundary the same way, so wherever OpenTelemetry's semantic conventions have a word for something in a binding, that is the word suss writes.
49
+
50
+ The values are theirs. A Postgres table is `postgresql`, not `postgres`; a DynamoDB table is `aws.dynamodb`; an SQS queue is `aws_sqs` and an SNS topic is `aws.sns`, spelled the two different ways the conventions spell them.
51
+
52
+ The field names are ours. Each protocol module says which attribute each of its fields goes under, and `semconvAttributes(binding)` reads a binding as the attributes a span would state:
53
+
54
+ ```ts
55
+ semconvAttributes(
56
+ storageBinding({
57
+ recognition: "prisma",
58
+ storageSystem: "postgresql",
59
+ scope: "orders",
60
+ container: "users",
61
+ }),
62
+ );
63
+ // { "db.system.name": "postgresql", "db.namespace": "orders",
64
+ // "db.collection.name": "users" }
65
+ ```
66
+
67
+ A field appears in that projection only when the value suss writes is the value a span gets, so the comparison is byte for byte with nothing in between. Three kinds of field stay out of it:
68
+
69
+ - **A field the conventions have no attribute for.** A secondary index (`accessPath`) is one, and a GraphQL resolver's type and field are another, since the conventions describe the operation a client sent and not the resolver the server ran for one field of it.
70
+ - **A value suss writes where the source stated none.** `scope: "default"` means no source said which database, and a REST method of `"*"` means the route responds to every method. A span says neither.
71
+ - **The same thing under a different string.** `service.name` and `cloud.resource_id` both point at the deployable a `runtime-config` boundary belongs to, but `instanceName` is the deployment template's logical id, which is neither of those strings.
72
+
73
+ Where the conventions have no word at all, suss keeps its own: a store they never covered (`s3`, `gcs`, `r2`, `d1`, `cloudflare-kv`), a bus they never covered (`eventbridge`, `bullmq`, `nats`, the Cloudflare triggers), a metric's system and type, and every boundary nobody crosses at run time, which is a function call across a package boundary, a contract a template declares, and an intent. Those are the boundaries suss exists for, and no observability convention has a word for them.
74
+
75
+ Adding a protocol means filling in `semconv` on its definition, empty included, so the question gets answered rather than skipped.
76
+
34
77
  ## Where it fits in suss
35
78
 
36
79
  Both `@suss/behavioral-ir` (what code does) and `@suss/intent-ir` (what the team meant) build on this package, so neither IR depends on the other. They describe boundaries in the same vocabulary, and suss compares them rather than merging them. `@suss/behavioral-ir` re-exports these primitives, so existing consumers keep importing them from there unchanged.
@@ -1,8 +1,8 @@
1
1
  // src/schemas.ts
2
- import { z as z10 } from "zod";
2
+ import { z as z11 } from "zod";
3
3
 
4
4
  // src/semantics/registry.ts
5
- import { z as z9 } from "zod";
5
+ import { z as z10 } from "zod";
6
6
 
7
7
  // src/semantics/functionCall.ts
8
8
  import { z } from "zod";
@@ -17,6 +17,9 @@ function fnIdentityKey(packageName, exportPath) {
17
17
  function busIdentityKey(messageBus, subject) {
18
18
  return `bus:${messageBus} ${subject}`;
19
19
  }
20
+ function metricIdentityKey(metricSystem, metricType) {
21
+ return `metric:${metricSystem} ${metricType}`;
22
+ }
20
23
 
21
24
  // src/semantics/definition.ts
22
25
  function defineBoundarySemantics(definition) {
@@ -58,6 +61,9 @@ var FunctionCallSemanticsSchema = z.object({
58
61
  var functionCallSemantics = defineBoundarySemantics({
59
62
  name: "function-call",
60
63
  schema: FunctionCallSemanticsSchema,
64
+ // A call that never leaves the process does not produce a span, and
65
+ // the conventions do not name one. All of it is ours.
66
+ semconv: {},
61
67
  behavior: {
62
68
  /** A call returns a value, which is not a status and a body. */
63
69
  exchangesHttpResponses: false,
@@ -93,6 +99,10 @@ var GraphqlOperationSemanticsSchema = z2.object({
93
99
  var graphqlOperationSemantics = defineBoundarySemantics({
94
100
  name: "graphql-operation",
95
101
  schema: GraphqlOperationSemanticsSchema,
102
+ semconv: {
103
+ operationName: { name: "graphql.operation.name" },
104
+ operationType: { name: "graphql.operation.type" }
105
+ },
96
106
  behavior: {
97
107
  /**
98
108
  * An operation gets a data-and-errors document back, which the
@@ -126,6 +136,9 @@ var GraphqlResolverSemanticsSchema = z3.object({
126
136
  var graphqlResolverSemantics = defineBoundarySemantics({
127
137
  name: "graphql-resolver",
128
138
  schema: GraphqlResolverSemanticsSchema,
139
+ // The conventions name the operation a client sent, not the
140
+ // resolver the server ran for one field of it.
141
+ semconv: {},
129
142
  behavior: {
130
143
  /** A resolver returns a field value, not a status and a body. */
131
144
  exchangesHttpResponses: false,
@@ -174,8 +187,11 @@ function channelsPair(a, b) {
174
187
  var MessageBusSemanticsSchema = z4.object({
175
188
  name: z4.literal("message-bus"),
176
189
  messageBus: z4.enum([
177
- "sqs",
178
- "sns",
190
+ // The conventions spell the two AWS buses this way, one with an
191
+ // underscore and one with a dot, so these are their strings and
192
+ // not a typo.
193
+ "aws_sqs",
194
+ "aws.sns",
179
195
  "s3",
180
196
  "eventbridge",
181
197
  "bullmq",
@@ -196,6 +212,10 @@ var MessageBusSemanticsSchema = z4.object({
196
212
  var messageBusSemantics = defineBoundarySemantics({
197
213
  name: "message-bus",
198
214
  schema: MessageBusSemanticsSchema,
215
+ semconv: {
216
+ messageBus: { name: "messaging.system" },
217
+ channel: { name: "messaging.destination.name" }
218
+ },
199
219
  behavior: {
200
220
  /** A message goes onto the channel and nothing comes back. */
201
221
  exchangesHttpResponses: false,
@@ -254,25 +274,58 @@ var messageBusSemantics = defineBoundarySemantics({
254
274
  }
255
275
  });
256
276
 
257
- // src/semantics/rest.ts
277
+ // src/semantics/metric.ts
258
278
  import { z as z5 } from "zod";
259
- var RestSemanticsSchema = z5.object({
260
- name: z5.literal("rest"),
279
+ var MetricSemanticsSchema = z5.object({
280
+ name: z5.literal("metric"),
281
+ /** Which system the series lives in: cloud-monitoring, cloudwatch. */
282
+ metricSystem: z5.string(),
283
+ /**
284
+ * The type string the system knows the metric by, spelled the way
285
+ * both sides spell it. Null when the source states one this reader
286
+ * could not settle, as with a query it could not read: a metric
287
+ * nobody could name pairs with nothing rather than with whatever
288
+ * happens to share its source text.
289
+ */
290
+ metricType: z5.string().nullable()
291
+ });
292
+ var metricSemantics = defineBoundarySemantics({
293
+ name: "metric",
294
+ schema: MetricSemanticsSchema,
295
+ // OpenTelemetry knows a metric by its instrument name, and has no
296
+ // attribute for the system it lives in or the type string it goes by.
297
+ semconv: {},
298
+ behavior: {
299
+ exchangesHttpResponses: false,
300
+ reportsUnpairedItself: false,
301
+ identityKey(semantics) {
302
+ if (semantics.metricType === null) {
303
+ return null;
304
+ }
305
+ return metricIdentityKey(semantics.metricSystem, semantics.metricType);
306
+ }
307
+ }
308
+ });
309
+
310
+ // src/semantics/rest.ts
311
+ import { z as z6 } from "zod";
312
+ var RestSemanticsSchema = z6.object({
313
+ name: z6.literal("rest"),
261
314
  /**
262
315
  * Uppercase HTTP method ("GET", "POST", …), `"*"` for a handler that
263
316
  * responds to every method, or null when this source does not say
264
317
  * which method.
265
318
  */
266
- method: z5.string().min(1).nullable(),
319
+ method: z6.string().min(1).nullable(),
267
320
  /** Normalized route path ("/users/{id}"), or null when this source does not give one. */
268
- path: z5.string().min(1).nullable(),
321
+ path: z6.string().min(1).nullable(),
269
322
  /**
270
323
  * Status codes the producing source explicitly declared (OpenAPI
271
324
  * responses, CFN MethodResponses, ts-rest router statuses). They are
272
325
  * kept here so the pairing layer can see them without unwrapping any
273
326
  * metadata. Absent, or empty, for a source that was inferred.
274
327
  */
275
- declaredResponses: z5.array(z5.number()).optional()
328
+ declaredResponses: z6.array(z6.number()).optional()
276
329
  });
277
330
  function normalizePath(path) {
278
331
  let normalized = path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
@@ -311,6 +364,14 @@ function routePathAdmits(declaredPath, requestPath) {
311
364
  var restSemantics = defineBoundarySemantics({
312
365
  name: "rest",
313
366
  schema: RestSemanticsSchema,
367
+ semconv: {
368
+ // A span states the one method a request used, so a route that
369
+ // responds to every method has nothing to compare.
370
+ method: { name: "http.request.method", placeholderValues: ["*"] },
371
+ path: { name: "http.route" }
372
+ // declaredResponses is a list, and http.response.status_code is
373
+ // the one status a request got back.
374
+ },
314
375
  behavior: {
315
376
  /** A request goes out, a status and a body come back. */
316
377
  exchangesHttpResponses: true,
@@ -383,12 +444,12 @@ var restSemantics = defineBoundarySemantics({
383
444
  });
384
445
 
385
446
  // src/semantics/runtimeConfig.ts
386
- import { z as z7 } from "zod";
447
+ import { z as z8 } from "zod";
387
448
 
388
449
  // src/deployableUnit.ts
389
- import { z as z6 } from "zod";
390
- var DeployableUnitSchema = z6.object({
391
- deploymentTarget: z6.enum([
450
+ import { z as z7 } from "zod";
451
+ var DeployableUnitSchema = z7.object({
452
+ deploymentTarget: z7.enum([
392
453
  "lambda",
393
454
  "ecs-task",
394
455
  "container",
@@ -397,7 +458,7 @@ var DeployableUnitSchema = z6.object({
397
458
  ]),
398
459
  // An empty name would agree with every other empty name, so a unit
399
460
  // with no name has to leave the field off instead.
400
- instanceName: z6.string().min(1)
461
+ instanceName: z7.string().min(1)
401
462
  });
402
463
  function ecsContainerInstanceName(taskDefinitionLogicalId, containerName) {
403
464
  return `${taskDefinitionLogicalId}/${containerName}`;
@@ -405,11 +466,15 @@ function ecsContainerInstanceName(taskDefinitionLogicalId, containerName) {
405
466
 
406
467
  // src/semantics/runtimeConfig.ts
407
468
  var RuntimeConfigSemanticsSchema = DeployableUnitSchema.extend({
408
- name: z7.literal("runtime-config")
469
+ name: z8.literal("runtime-config")
409
470
  });
410
471
  var runtimeConfigSemantics = defineBoundarySemantics({
411
472
  name: "runtime-config",
412
473
  schema: RuntimeConfigSemanticsSchema,
474
+ // service.name and cloud.resource_id name the same deployable, but
475
+ // instanceName is the deployment template's logical id, which is
476
+ // neither of those strings.
477
+ semconv: {},
413
478
  behavior: {
414
479
  /** A process reads its config at startup, and nothing replies. */
415
480
  exchangesHttpResponses: false,
@@ -419,28 +484,28 @@ var runtimeConfigSemantics = defineBoundarySemantics({
419
484
  });
420
485
 
421
486
  // src/semantics/storage.ts
422
- import { z as z8 } from "zod";
423
- var StorageSemanticsSchema = z8.object({
424
- name: z8.literal("storage"),
487
+ import { z as z9 } from "zod";
488
+ var StorageSemanticsSchema = z9.object({
489
+ name: z9.literal("storage"),
425
490
  /**
426
- * Which store this is: postgres, mysql, sqlite, dynamodb. Two
427
- * products' containers can share a name, so this keeps them apart,
428
- * and it is what a finding message calls the store.
491
+ * Which store this is: postgresql, mysql, aws.dynamodb, or our own
492
+ * word for one OpenTelemetry never named, such as s3. Two products'
493
+ * containers can share a name, so this keeps them apart.
429
494
  */
430
- storageSystem: z8.string(),
495
+ storageSystem: z9.string(),
431
496
  /**
432
497
  * The ORM, schema, or deployment scope. A single-database setup uses
433
498
  * `"default"`, and a monorepo with several schemas gives each one its
434
499
  * own value.
435
500
  */
436
- scope: z8.string(),
501
+ scope: z9.string(),
437
502
  /**
438
503
  * The table, bucket, collection, or index, as the source declares
439
504
  * it, or null when the source states one this reader could not
440
505
  * settle. A null container pairs with nothing, rather than with
441
506
  * whatever its source text happens to spell.
442
507
  */
443
- container: z8.string().nullable(),
508
+ container: z9.string().nullable(),
444
509
  /**
445
510
  * A secondary way into the container, a DynamoDB global secondary
446
511
  * index or an Elasticsearch alias, each with its own key fields.
@@ -448,11 +513,19 @@ var StorageSemanticsSchema = z8.object({
448
513
  * index and a query through the table are different accesses, so
449
514
  * they pair separately.
450
515
  */
451
- accessPath: z8.string().nullable()
516
+ accessPath: z9.string().nullable()
452
517
  });
453
518
  var storageSemantics = defineBoundarySemantics({
454
519
  name: "storage",
455
520
  schema: StorageSemanticsSchema,
521
+ semconv: {
522
+ storageSystem: { name: "db.system.name" },
523
+ // "default" is our word for a source that named no database.
524
+ scope: { name: "db.namespace", placeholderValues: ["default"] },
525
+ container: { name: "db.collection.name" }
526
+ // A secondary index has no attribute of its own, so accessPath
527
+ // stays our word.
528
+ },
456
529
  behavior: {
457
530
  /** A query returns rows or items, not a status and a body. */
458
531
  exchangesHttpResponses: false,
@@ -462,14 +535,15 @@ var storageSemantics = defineBoundarySemantics({
462
535
  });
463
536
 
464
537
  // src/semantics/registry.ts
465
- var SemanticsSchema = z9.discriminatedUnion("name", [
538
+ var SemanticsSchema = z10.discriminatedUnion("name", [
466
539
  restSemantics.schema,
467
540
  functionCallSemantics.schema,
468
541
  graphqlResolverSemantics.schema,
469
542
  graphqlOperationSemantics.schema,
470
543
  runtimeConfigSemantics.schema,
471
544
  storageSemantics.schema,
472
- messageBusSemantics.schema
545
+ messageBusSemantics.schema,
546
+ metricSemantics.schema
473
547
  ]);
474
548
  var DEFINITIONS = [
475
549
  restSemantics,
@@ -478,48 +552,55 @@ var DEFINITIONS = [
478
552
  graphqlOperationSemantics,
479
553
  runtimeConfigSemantics,
480
554
  storageSemantics,
481
- messageBusSemantics
555
+ messageBusSemantics,
556
+ metricSemantics
482
557
  ];
483
558
  var BY_NAME = new Map(
484
559
  DEFINITIONS.map((d) => [d.name, d])
485
560
  );
486
561
  function behaviorOf(semantics) {
487
- const definition = BY_NAME.get(semantics.name);
562
+ return definitionFor(semantics.name).behavior;
563
+ }
564
+ function definitionFor(name) {
565
+ const definition = BY_NAME.get(name);
488
566
  if (definition === void 0) {
489
- throw new Error(`no boundary behavior for semantics "${semantics.name}"`);
567
+ throw new Error(`no boundary definition for semantics "${name}"`);
490
568
  }
491
- return definition.behavior;
569
+ return definition;
570
+ }
571
+ function semconvMappingOf(semantics) {
572
+ return definitionFor(semantics.name).semconv;
492
573
  }
493
574
  function allBehaviors() {
494
575
  return DEFINITIONS.map((d) => d.behavior);
495
576
  }
496
577
 
497
578
  // src/schemas.ts
498
- var ConfidenceSourceSchema = z10.enum([
579
+ var ConfidenceSourceSchema = z11.enum([
499
580
  "inferred_static",
500
581
  "inferred_ai",
501
582
  "declared",
502
583
  "derived"
503
584
  ]);
504
- var ConfidenceLevelSchema = z10.enum(["high", "medium", "low"]);
505
- var CorroborationSchema = z10.object({
506
- outcome: z10.enum(["observed", "refuted", "untested"]),
585
+ var ConfidenceLevelSchema = z11.enum(["high", "medium", "low"]);
586
+ var CorroborationSchema = z11.object({
587
+ outcome: z11.enum(["observed", "refuted", "untested"]),
507
588
  /** Executions that produced a verdict for this claim. */
508
- runs: z10.number(),
589
+ runs: z11.number(),
509
590
  /** Present when refuted: the input and observation that disagreed. */
510
- counterexample: z10.unknown().optional(),
591
+ counterexample: z11.unknown().optional(),
511
592
  /** Present when untested: why no verdict was reachable. */
512
- reason: z10.string().optional()
593
+ reason: z11.string().optional()
513
594
  });
514
- var ConfidenceInfoSchema = z10.object({
595
+ var ConfidenceInfoSchema = z11.object({
515
596
  source: ConfidenceSourceSchema,
516
597
  level: ConfidenceLevelSchema,
517
598
  corroboration: CorroborationSchema.optional()
518
599
  });
519
- var SourceLocationSchema = z10.object({
520
- file: z10.string(),
521
- range: z10.object({ start: z10.number(), end: z10.number() }),
522
- exportName: z10.string().nullable(),
600
+ var SourceLocationSchema = z11.object({
601
+ file: z11.string(),
602
+ range: z11.object({ start: z11.number(), end: z11.number() }),
603
+ exportName: z11.string().nullable(),
523
604
  /**
524
605
  * The project the extract was pointed at, by the name that project
525
606
  * calls itself.
@@ -529,47 +610,47 @@ var SourceLocationSchema = z10.object({
529
610
  * summaries puts them on top of each other. This field is what tells
530
611
  * them apart, and what a reader groups by.
531
612
  */
532
- workspace: z10.string().optional()
613
+ workspace: z11.string().optional()
533
614
  });
534
- var BoundaryBindingSchema = z10.object({
535
- transport: z10.string(),
615
+ var BoundaryBindingSchema = z11.object({
616
+ transport: z11.string(),
536
617
  semantics: SemanticsSchema,
537
- recognition: z10.string()
618
+ recognition: z11.string()
538
619
  });
539
620
  function typeDefinitionKey(ref) {
540
621
  return ref.def ?? null;
541
622
  }
542
- var TypeShapeSchema = z10.lazy(
543
- () => z10.discriminatedUnion("type", [
544
- z10.object({
545
- type: z10.literal("record"),
546
- properties: z10.record(z10.string(), TypeShapeSchema),
547
- spreads: z10.array(z10.object({ sourceText: z10.string() })).optional()
623
+ var TypeShapeSchema = z11.lazy(
624
+ () => z11.discriminatedUnion("type", [
625
+ z11.object({
626
+ type: z11.literal("record"),
627
+ properties: z11.record(z11.string(), TypeShapeSchema),
628
+ spreads: z11.array(z11.object({ sourceText: z11.string() })).optional()
548
629
  }),
549
- z10.object({ type: z10.literal("dictionary"), values: TypeShapeSchema }),
550
- z10.object({ type: z10.literal("array"), items: TypeShapeSchema }),
551
- z10.object({
552
- type: z10.literal("literal"),
553
- value: z10.union([z10.string(), z10.number(), z10.boolean()]),
554
- raw: z10.string().optional()
630
+ z11.object({ type: z11.literal("dictionary"), values: TypeShapeSchema }),
631
+ z11.object({ type: z11.literal("array"), items: TypeShapeSchema }),
632
+ z11.object({
633
+ type: z11.literal("literal"),
634
+ value: z11.union([z11.string(), z11.number(), z11.boolean()]),
635
+ raw: z11.string().optional()
555
636
  }),
556
- z10.object({ type: z10.literal("text") }),
557
- z10.object({ type: z10.literal("integer") }),
558
- z10.object({ type: z10.literal("number") }),
559
- z10.object({ type: z10.literal("boolean") }),
560
- z10.object({ type: z10.literal("null") }),
561
- z10.object({ type: z10.literal("undefined") }),
562
- z10.object({
563
- type: z10.literal("union"),
564
- variants: z10.array(TypeShapeSchema)
637
+ z11.object({ type: z11.literal("text") }),
638
+ z11.object({ type: z11.literal("integer") }),
639
+ z11.object({ type: z11.literal("number") }),
640
+ z11.object({ type: z11.literal("boolean") }),
641
+ z11.object({ type: z11.literal("null") }),
642
+ z11.object({ type: z11.literal("undefined") }),
643
+ z11.object({
644
+ type: z11.literal("union"),
645
+ variants: z11.array(TypeShapeSchema)
565
646
  }),
566
- z10.object({
567
- type: z10.literal("ref"),
568
- name: z10.string(),
569
- from: z10.string().optional(),
570
- def: z10.string().optional()
647
+ z11.object({
648
+ type: z11.literal("ref"),
649
+ name: z11.string(),
650
+ from: z11.string().optional(),
651
+ def: z11.string().optional()
571
652
  }),
572
- z10.object({ type: z10.literal("unknown") })
653
+ z11.object({ type: z11.literal("unknown") })
573
654
  ])
574
655
  );
575
656
  var MAX_DEFINITION_DEPTH = 6;
@@ -618,6 +699,7 @@ export {
618
699
  gqlIdentityKey,
619
700
  fnIdentityKey,
620
701
  busIdentityKey,
702
+ metricIdentityKey,
621
703
  FunctionCallSemanticsSchema,
622
704
  GraphqlResolverSemanticsSchema,
623
705
  parseChannel,
@@ -625,6 +707,7 @@ export {
625
707
  busesAgree,
626
708
  channelsPair,
627
709
  MessageBusSemanticsSchema,
710
+ MetricSemanticsSchema,
628
711
  RestSemanticsSchema,
629
712
  normalizePath,
630
713
  methodsAgree,
@@ -635,6 +718,7 @@ export {
635
718
  StorageSemanticsSchema,
636
719
  SemanticsSchema,
637
720
  behaviorOf,
721
+ semconvMappingOf,
638
722
  allBehaviors,
639
723
  ConfidenceSourceSchema,
640
724
  ConfidenceLevelSchema,