@suss/ir-core 0.33.0 → 0.34.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/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  FunctionCallSemanticsSchema: () => FunctionCallSemanticsSchema,
32
32
  GraphqlOperationSemanticsSchema: () => GraphqlOperationSemanticsSchema,
33
33
  GraphqlResolverSemanticsSchema: () => GraphqlResolverSemanticsSchema,
34
+ IoSemanticsSchema: () => IoSemanticsSchema,
34
35
  MessageBusSemanticsSchema: () => MessageBusSemanticsSchema,
35
36
  MetricSemanticsSchema: () => MetricSemanticsSchema,
36
37
  NOTHING_DEPLOYED: () => NOTHING_DEPLOYED,
@@ -43,6 +44,7 @@ __export(index_exports, {
43
44
  StorageSemanticsSchema: () => StorageSemanticsSchema,
44
45
  SuppressionFileSchema: () => SuppressionFileSchema,
45
46
  SuppressionRuleSchema: () => SuppressionRuleSchema,
47
+ TEST_TITLE_SEPARATOR: () => TEST_TITLE_SEPARATOR,
46
48
  TypeShapeSchema: () => TypeShapeSchema,
47
49
  UnitInvocationSemanticsSchema: () => UnitInvocationSemanticsSchema,
48
50
  applySuppressionsToFindings: () => applySuppressionsToFindings,
@@ -76,12 +78,17 @@ __export(index_exports, {
76
78
  groundBinding: () => groundBinding,
77
79
  groundedPairingKey: () => groundedPairingKey,
78
80
  hasNameHole: () => hasNameHole,
81
+ ioBinding: () => ioBinding,
82
+ ioIdentityKey: () => ioIdentityKey,
79
83
  isGraphqlOperationBinding: () => isGraphqlOperationBinding,
84
+ isModuleName: () => isModuleName,
85
+ labelWithDetail: () => labelWithDetail,
80
86
  leavesTheProcess: () => leavesTheProcess,
81
87
  messageBusBinding: () => messageBusBinding,
82
88
  methodsAgree: () => methodsAgree,
83
89
  metricBinding: () => metricBinding,
84
90
  metricIdentityKey: () => metricIdentityKey,
91
+ moduleExportBinding: () => moduleExportBinding,
85
92
  namePatternFromSub: () => namePatternFromSub,
86
93
  namePatternKey: () => namePatternKey,
87
94
  nameReference: () => nameReference,
@@ -121,6 +128,7 @@ __export(index_exports, {
121
128
  storageContainerLabel: () => storageContainerLabel,
122
129
  storageLabel: () => storageLabel,
123
130
  storageSystemLabel: () => storageSystemLabel,
131
+ testUnitName: () => testUnitName,
124
132
  typeDefinitionKey: () => typeDefinitionKey,
125
133
  unitIdentityKey: () => unitIdentityKey,
126
134
  unitInvocationBinding: () => unitInvocationBinding,
@@ -156,15 +164,15 @@ var graphqlOperationSemantics = defineBoundarySemantics({
156
164
  behavior: {
157
165
  /**
158
166
  * An operation gets a data-and-errors document back, which the
159
- * GraphQL contract checker reads. Status codes say nothing here.
167
+ * GraphQL contract checker reads. Status codes do not apply.
160
168
  */
161
169
  exchangesHttpResponses: false,
162
170
  leavesTheProcess: true,
163
171
  reportsUnpairedItself: false,
164
172
  identityKey: () => null,
165
- /** Keyless by design; the dedicated pass pairs it by its document. */
173
+ /** No key on purpose. The dedicated pass pairs an operation by its document. */
166
174
  canPair: () => true,
167
- /** "query GetUser". The contract checker pairs it, the key does not. */
175
+ /** `query GetUser`. Only for display, since the contract checker does the pairing. */
168
176
  displayLabel(semantics) {
169
177
  return `${semantics.operationType} ${semantics.operationName ?? "<anonymous>"}`;
170
178
  }
@@ -187,22 +195,26 @@ function unitIdentityKey(deploymentTarget, instanceName) {
187
195
  function metricIdentityKey(metricSystem, metricType) {
188
196
  return `metric:${metricSystem} ${metricType}`;
189
197
  }
198
+ function ioIdentityKey(target) {
199
+ return `io:${target}`;
200
+ }
190
201
 
191
202
  // src/schemas.ts
192
- var import_zod12 = require("zod");
203
+ var import_zod13 = require("zod");
193
204
 
194
205
  // src/semantics/registry.ts
195
- var import_zod11 = require("zod");
206
+ var import_zod12 = require("zod");
196
207
 
197
208
  // src/semantics/functionCall.ts
198
209
  var import_zod2 = require("zod");
199
210
  var FunctionCallSemanticsSchema = import_zod2.z.object({
200
211
  name: import_zod2.z.literal("function-call"),
201
212
  /**
202
- * Optional module identifier for cross-unit references
203
- * (e.g. `"./components/Button"` for a React component, or the TS
204
- * module path for a bare function export). Packs that don't do
205
- * cross-module pairing can leave it unset.
213
+ * Where the function lives inside this repository. It is either the
214
+ * name of a module the project lists in `suss.json`, such as
215
+ * `"billing"`, or a module path, such as the file a server action is
216
+ * written in. A name has no separator in it and a path always does.
217
+ * Packs that don't do cross-module pairing can leave it unset.
206
218
  */
207
219
  module: import_zod2.z.string().optional(),
208
220
  /** Named export within the module, when applicable. */
@@ -227,35 +239,38 @@ var FunctionCallSemanticsSchema = import_zod2.z.object({
227
239
  */
228
240
  exportPath: import_zod2.z.array(import_zod2.z.string()).optional()
229
241
  });
242
+ function isModuleName(module2) {
243
+ return !module2.includes("/") && !module2.includes("\\");
244
+ }
230
245
  var functionCallSemantics = defineBoundarySemantics({
231
246
  name: "function-call",
232
247
  schema: FunctionCallSemanticsSchema,
233
- // A call that never leaves the process does not produce a span, and
234
- // the conventions do not name one. All of it is ours.
248
+ // A call inside the process does not produce a span, and the
249
+ // conventions have no attributes for it.
235
250
  semconv: {},
236
251
  behavior: {
237
- /** A call returns a value, which is not a status and a body. */
252
+ /** A call returns a value, with no status. */
238
253
  exchangesHttpResponses: false,
239
254
  leavesTheProcess: false,
240
255
  reportsUnpairedItself: false,
241
256
  /**
242
- * In-repo units (components, bare handlers) are keyless by design
243
- * and pair through call edges rather than the keyed pass, so a
244
- * missing key is not a boundary nothing can reach.
257
+ * In-repo units such as components have no key on purpose. They
258
+ * pair through call edges instead of the keyed pass, so a missing
259
+ * key does not make them unreachable.
245
260
  */
246
261
  canPair: () => true,
247
- /**
248
- * `"fn:<package>::<exportPath>"` when both are set, and
249
- * `"fn:<module>::<exportName>"` for an in-repo unit that states its
250
- * module, the way a server action does. Components and bare
251
- * handlers state neither and have no key at all.
252
- */
253
262
  rewritePaths(semantics, rewrite) {
254
- if (semantics.module === void 0) {
263
+ if (semantics.module === void 0 || isModuleName(semantics.module)) {
255
264
  return semantics;
256
265
  }
257
266
  return { ...semantics, module: rewrite(semantics.module) };
258
267
  },
268
+ /**
269
+ * `"fn:<package>::<exportPath>"` when both are set, and
270
+ * `"fn:<module>::<exportName>"` for an in-repo unit that gives its
271
+ * module, as a server action does. Components and plain handlers
272
+ * give neither and have no key.
273
+ */
259
274
  identityKey(semantics) {
260
275
  if (semantics.package !== void 0 && semantics.exportPath !== void 0 && semantics.exportPath.length > 0) {
261
276
  return fnIdentityKey(semantics.package, semantics.exportPath);
@@ -284,14 +299,14 @@ var GraphqlResolverSemanticsSchema = import_zod3.z.object({
284
299
  var graphqlResolverSemantics = defineBoundarySemantics({
285
300
  name: "graphql-resolver",
286
301
  schema: GraphqlResolverSemanticsSchema,
287
- // The conventions name the operation a client sent, not the
288
- // resolver the server ran for one field of it.
302
+ // The conventions describe the operation a client sent. They have no
303
+ // attribute for the resolver the server ran for one field of it.
289
304
  semconv: {},
290
305
  behavior: {
291
- /** A resolver returns a field value, not a status and a body. */
306
+ /** A resolver returns a field value, with no status. */
292
307
  exchangesHttpResponses: false,
293
- // A query somebody sent from outside runs it, even though the
294
- // server calls the function in its own process.
308
+ // A resolver runs because a client outside the process sent a query,
309
+ // even though the server calls it in its own process.
295
310
  leavesTheProcess: true,
296
311
  reportsUnpairedItself: false,
297
312
  /**
@@ -308,8 +323,32 @@ var graphqlResolverSemantics = defineBoundarySemantics({
308
323
  }
309
324
  });
310
325
 
311
- // src/semantics/messageBus.ts
326
+ // src/semantics/io.ts
312
327
  var import_zod4 = require("zod");
328
+ var IoSemanticsSchema = import_zod4.z.object({
329
+ name: import_zod4.z.literal("io"),
330
+ /** The stream, such as `stdout` or `stderr`. Null when the source does not say which. */
331
+ target: import_zod4.z.string().nullable()
332
+ });
333
+ var ioSemantics = defineBoundarySemantics({
334
+ name: "io",
335
+ schema: IoSemanticsSchema,
336
+ // The semantic conventions have no attribute for a process's own streams.
337
+ semconv: {},
338
+ behavior: {
339
+ exchangesHttpResponses: false,
340
+ leavesTheProcess: true,
341
+ reportsUnpairedItself: false,
342
+ identityKey(semantics) {
343
+ return semantics.target === null ? null : ioIdentityKey(semantics.target);
344
+ },
345
+ // Nothing declares the reading side, so a write has nothing to meet.
346
+ canPair: () => false
347
+ }
348
+ });
349
+
350
+ // src/semantics/messageBus.ts
351
+ var import_zod5 = require("zod");
313
352
 
314
353
  // src/channel.ts
315
354
  function parseChannel(channel) {
@@ -335,12 +374,11 @@ function channelsPair(a, b) {
335
374
  }
336
375
 
337
376
  // src/semantics/messageBus.ts
338
- var MessageBusSemanticsSchema = import_zod4.z.object({
339
- name: import_zod4.z.literal("message-bus"),
340
- messageBus: import_zod4.z.enum([
341
- // The conventions spell the two AWS buses this way, one with an
342
- // underscore and one with a dot, so these are their strings and
343
- // not a typo.
377
+ var MessageBusSemanticsSchema = import_zod5.z.object({
378
+ name: import_zod5.z.literal("message-bus"),
379
+ messageBus: import_zod5.z.enum([
380
+ // The conventions write these two AWS buses differently, one with an
381
+ // underscore and one with a dot. Both are as the conventions give them.
344
382
  "aws_sqs",
345
383
  "aws.sns",
346
384
  "s3",
@@ -359,12 +397,13 @@ var MessageBusSemanticsSchema = import_zod4.z.object({
359
397
  "cloudflare-tail"
360
398
  ]),
361
399
  /**
362
- * Stable channel identifier: a CFN logical id, a queue or topic name,
363
- * a subject pattern, or `bus#detailType`. Null when this source does
364
- * not say which channel, as with a send whose queue the code picks at
365
- * runtime, or a receive whose queue the event-source mapping states.
400
+ * A stable channel identifier: a CloudFormation logical id, a queue or
401
+ * topic name, a subject pattern, or `bus#detailType`. Null when this
402
+ * source does not say which channel, as with a send whose queue the
403
+ * code picks at run time, or a receive whose queue is set in the
404
+ * event-source mapping.
366
405
  */
367
- channel: import_zod4.z.string().min(1).nullable()
406
+ channel: import_zod5.z.string().min(1).nullable()
368
407
  });
369
408
  var messageBusSemantics = defineBoundarySemantics({
370
409
  name: "message-bus",
@@ -378,25 +417,26 @@ var messageBusSemantics = defineBoundarySemantics({
378
417
  exchangesHttpResponses: false,
379
418
  leavesTheProcess: true,
380
419
  /**
381
- * `checkMessageBus` looks at every channel, including the unused
382
- * ones, so the generic unmatched lists leave them alone.
420
+ * `checkMessageBus` reports every channel, unused ones included, so
421
+ * the generic unmatched list leaves them out.
383
422
  */
384
423
  reportsUnpairedItself: true,
385
424
  /**
386
- * A channel unnamed here can still pair: the dedicated pass
387
- * collapses env chains before comparing, so a consumer whose
388
- * channel arrives at runtime meets the template that declares it.
425
+ * A channel that is null here can still pair. The dedicated pass
426
+ * follows environment variable chains before comparing, so a
427
+ * consumer whose channel is set at run time meets the template that
428
+ * declares it.
389
429
  */
390
430
  canPair: () => true,
391
431
  /**
392
432
  * `"bus:<messageBus> <subject>"`, or null when the channel is null
393
433
  * or its subject is empty.
394
434
  *
395
- * The key has the subject in it and leaves out the bus, so a
396
- * template writing `default#order.placed` and a handler writing
435
+ * The key has the subject and leaves out the bus, so a template
436
+ * writing `default#order.placed` and a handler writing
397
437
  * `order.placed` land in one bucket, where `sidesAgree` compares the
398
- * buses. A side that cannot know its bus would otherwise be keyed
399
- * away from the side that can.
438
+ * buses. Keying on the bus would separate a side that cannot know
439
+ * its bus from the side that can.
400
440
  *
401
441
  * The bus technology stays in, because a queue and an event router
402
442
  * are different destinations even with the same subject. Case stays
@@ -419,9 +459,9 @@ var messageBusSemantics = defineBoundarySemantics({
419
459
  return channelsPair(a.channel, b.channel);
420
460
  },
421
461
  /**
422
- * The whole channel, bus included, unlike the identity key, which
423
- * has the subject alone. Someone reading a list of unmatched
424
- * channels wants to see which bus each one was on.
462
+ * The whole channel, bus included, where the identity key has only
463
+ * the subject. A reader looking at unmatched channels needs to see
464
+ * which bus each one was on.
425
465
  */
426
466
  displayLabel(semantics) {
427
467
  if (semantics.channel === null) {
@@ -433,25 +473,25 @@ var messageBusSemantics = defineBoundarySemantics({
433
473
  });
434
474
 
435
475
  // src/semantics/metric.ts
436
- var import_zod5 = require("zod");
437
- var MetricSemanticsSchema = import_zod5.z.object({
438
- name: import_zod5.z.literal("metric"),
439
- /** Which system the series lives in: cloud-monitoring, cloudwatch. */
440
- metricSystem: import_zod5.z.string(),
476
+ var import_zod6 = require("zod");
477
+ var MetricSemanticsSchema = import_zod6.z.object({
478
+ name: import_zod6.z.literal("metric"),
479
+ /** The system the series is stored in, such as `cloud-monitoring` or `cloudwatch`. */
480
+ metricSystem: import_zod6.z.string(),
441
481
  /**
442
- * The type string the system knows the metric by, spelled the way
443
- * both sides spell it. Null when the source states one this reader
444
- * could not settle, as with a query it could not read: a metric
445
- * nobody could name pairs with nothing rather than with whatever
446
- * happens to share its source text.
482
+ * The type string the system uses for the metric, written the same
483
+ * way on both sides. Null when the reader could not work the string
484
+ * out, as with a query it could not parse. A null type pairs with
485
+ * nothing, so it cannot pair with an unrelated metric that happens to
486
+ * share its source text.
447
487
  */
448
- metricType: import_zod5.z.string().nullable()
488
+ metricType: import_zod6.z.string().nullable()
449
489
  });
450
490
  var metricSemantics = defineBoundarySemantics({
451
491
  name: "metric",
452
492
  schema: MetricSemanticsSchema,
453
- // OpenTelemetry knows a metric by its instrument name, and has no
454
- // attribute for the system it lives in or the type string it goes by.
493
+ // OpenTelemetry identifies a metric by its instrument name. It has no
494
+ // attribute for the metric's system or its type string.
455
495
  semconv: {},
456
496
  behavior: {
457
497
  exchangesHttpResponses: false,
@@ -467,7 +507,7 @@ var metricSemantics = defineBoundarySemantics({
467
507
  });
468
508
 
469
509
  // src/semantics/rest.ts
470
- var import_zod6 = require("zod");
510
+ var import_zod7 = require("zod");
471
511
 
472
512
  // src/dispatch.ts
473
513
  function dispatchByType(table, value) {
@@ -814,23 +854,23 @@ function compareRanks(a, b) {
814
854
  }
815
855
 
816
856
  // src/semantics/rest.ts
817
- var RestSemanticsSchema = import_zod6.z.object({
818
- name: import_zod6.z.literal("rest"),
857
+ var RestSemanticsSchema = import_zod7.z.object({
858
+ name: import_zod7.z.literal("rest"),
819
859
  /**
820
860
  * Uppercase HTTP method ("GET", "POST", …), `"*"` for a handler that
821
861
  * responds to every method, or null when this source does not say
822
862
  * which method.
823
863
  */
824
- method: import_zod6.z.string().min(1).nullable(),
864
+ method: import_zod7.z.string().min(1).nullable(),
825
865
  /** Normalized route path ("/users/{id}"), or null when this source does not give one. */
826
- path: import_zod6.z.string().min(1).nullable(),
866
+ path: import_zod7.z.string().min(1).nullable(),
827
867
  /**
828
- * Status codes the producing source explicitly declared (OpenAPI
829
- * responses, CFN MethodResponses, ts-rest router statuses). They are
830
- * kept here so the pairing layer can see them without unwrapping any
831
- * metadata. Absent, or empty, for a source that was inferred.
868
+ * Status codes the producing source declared explicitly, such as
869
+ * OpenAPI responses or CloudFormation MethodResponses. They are kept
870
+ * here so the pairing layer can see them without unwrapping any
871
+ * metadata. Absent or empty for an inferred source.
832
872
  */
833
- declaredResponses: import_zod6.z.array(import_zod6.z.number()).optional()
873
+ declaredResponses: import_zod7.z.array(import_zod7.z.number()).optional()
834
874
  });
835
875
  var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
836
876
  function baseUrlReference(semantics) {
@@ -887,16 +927,15 @@ var restSemantics = defineBoundarySemantics({
887
927
  return `${semantics.method.toUpperCase()} ${normalizePath(semantics.path)}`;
888
928
  },
889
929
  /**
890
- * The bucket has only the path in it, so a `"*"` route lands with
891
- * the consumers that each specify a method, and `methodsAgree`
892
- * settles it inside the bucket. The identity key stays what a reader
893
- * sees and a suppression targets ("GET /users", "* /users"), and
894
- * this key groups what a reader still tells apart.
930
+ * The bucket has only the path, so a `"*"` route lands with the
931
+ * consumers that each give a method, and `methodsAgree` compares the
932
+ * methods inside the bucket. The identity key is still what a reader
933
+ * sees and what a suppression targets ("GET /users", "* /users").
895
934
  *
896
- * A parameter's name goes too. A spec written by hand says
897
- * `{userId}` where the Express route that serves it says `:id`, and
898
- * both match the same requests, so pairing on the name gave two
899
- * boundaries that never met and a run with nothing to report.
935
+ * Parameter names are dropped too. A hand-written spec writes
936
+ * `{userId}` where the Express route that serves it writes `:id`,
937
+ * and both match the same requests. Pairing on the name would leave
938
+ * two boundaries that never meet.
900
939
  */
901
940
  pairingKey(semantics) {
902
941
  if (semantics.method === null || semantics.path === null) {
@@ -917,9 +956,9 @@ var restSemantics = defineBoundarySemantics({
917
956
  return semantics.path === null ? [] : pathSpecificity(normalizePath(semantics.path));
918
957
  },
919
958
  /**
920
- * What the identity key says, with a missing half still readable:
921
- * ANY when no method was stated, and ? when no path was. Null when
922
- * neither was stated, because then there is nothing to show.
959
+ * The identity key, with a missing half still readable: `ANY` when
960
+ * the source gave no method, and `?` when it gave no path. Null when
961
+ * both are missing, since then there is nothing to show.
923
962
  */
924
963
  displayLabel(semantics) {
925
964
  if (semantics.method === null && semantics.path === null) {
@@ -930,11 +969,11 @@ var restSemantics = defineBoundarySemantics({
930
969
  return `${method} ${path}`;
931
970
  },
932
971
  /**
933
- * A route with both halves stated decides for itself: the method
934
- * through `methodsAgree` (so a `"*"` route matches every method),
935
- * and the path through `routePathAdmits`. A route missing either
936
- * half might still be the one that handles the request, and nothing
937
- * here can settle that, so it abstains instead of refusing.
972
+ * With both halves present, the method goes through `methodsAgree`
973
+ * (so a `"*"` route matches every method) and the path through
974
+ * `routePathAdmits`. A route missing either half might still handle
975
+ * the request, and this declaration cannot settle that, so the
976
+ * result is `"unknown"`.
938
977
  */
939
978
  servesRequest(semantics, method, path) {
940
979
  if (semantics.method === null || semantics.path === null) {
@@ -947,8 +986,8 @@ var restSemantics = defineBoundarySemantics({
947
986
  },
948
987
  /**
949
988
  * A route the registration pattern admits is inside it. A route
950
- * whose path this source never gave is left outside, because
951
- * nothing here can show it is one the pattern covers.
989
+ * without a path counts as outside, because nothing shows that the
990
+ * pattern covers it.
952
991
  */
953
992
  withinScope(semantics, scope) {
954
993
  return semantics.path !== null && routePathAdmits(scope, semantics.path);
@@ -957,16 +996,12 @@ var restSemantics = defineBoundarySemantics({
957
996
  * A call whose base URL the deployment fills in, resolved to the
958
997
  * path it reaches.
959
998
  *
960
- * The source cannot settle this on its own, which is why the
961
- * adapter leaves the hole in. `API_BASE` could be
962
- * `http://backend.internal`, and then the path is `/orders`. It
963
- * could equally be `/api/v2`, and then the path is
964
- * `/api/v2/orders`. Putting the deployed value in and reading the
965
- * path back out gets the right one either way.
966
- *
967
- * Only a hole at the front is a base URL. A hole further along is a
968
- * route parameter, and `/orders/{id}` means every id rather than a
969
- * variable somebody set.
999
+ * The source cannot settle this on its own, so the adapter leaves
1000
+ * the hole in. `API_BASE` could be `http://backend.internal`, and
1001
+ * then the path is `/orders`. It could equally be `/api/v2`, and
1002
+ * then the path is `/api/v2/orders`. Filling in the deployed value
1003
+ * and reading the path back out gets the right one either way.
1004
+ * `baseUrlReference` finds the hole that is the base URL.
970
1005
  */
971
1006
  groundName(semantics, deployment) {
972
1007
  const reference = baseUrlReference(semantics);
@@ -983,7 +1018,7 @@ var restSemantics = defineBoundarySemantics({
983
1018
  },
984
1019
  nameReference: baseUrlReference,
985
1020
  ruleBoundary: {
986
- // "METHOD /path": one leading token, then an absolute path.
1021
+ // "METHOD /path": one token, a space, then an absolute path.
987
1022
  claims(raw) {
988
1023
  return /^\S+ +\//.test(raw.trim());
989
1024
  },
@@ -998,12 +1033,12 @@ var restSemantics = defineBoundarySemantics({
998
1033
  });
999
1034
 
1000
1035
  // src/semantics/runtimeConfig.ts
1001
- var import_zod8 = require("zod");
1036
+ var import_zod9 = require("zod");
1002
1037
 
1003
1038
  // src/deployableUnit.ts
1004
- var import_zod7 = require("zod");
1005
- var DeployableUnitSchema = import_zod7.z.object({
1006
- deploymentTarget: import_zod7.z.enum([
1039
+ var import_zod8 = require("zod");
1040
+ var DeployableUnitSchema = import_zod8.z.object({
1041
+ deploymentTarget: import_zod8.z.enum([
1007
1042
  "lambda",
1008
1043
  "ecs-task",
1009
1044
  "container",
@@ -1012,7 +1047,7 @@ var DeployableUnitSchema = import_zod7.z.object({
1012
1047
  ]),
1013
1048
  // An empty name would agree with every other empty name, so a unit
1014
1049
  // with no name has to leave the field off instead.
1015
- instanceName: import_zod7.z.string().min(1)
1050
+ instanceName: import_zod8.z.string().min(1)
1016
1051
  });
1017
1052
  function ecsContainerInstanceName(taskDefinitionLogicalId, containerName) {
1018
1053
  return `${taskDefinitionLogicalId}/${containerName}`;
@@ -1023,14 +1058,14 @@ var RuntimeConfigSemanticsSchema = DeployableUnitSchema.partial({
1023
1058
  deploymentTarget: true,
1024
1059
  instanceName: true
1025
1060
  }).extend({
1026
- name: import_zod8.z.literal("runtime-config")
1061
+ name: import_zod9.z.literal("runtime-config")
1027
1062
  });
1028
1063
  var runtimeConfigSemantics = defineBoundarySemantics({
1029
1064
  name: "runtime-config",
1030
1065
  schema: RuntimeConfigSemanticsSchema,
1031
- // service.name and cloud.resource_id name the same deployable, but
1032
- // instanceName is the deployment template's logical id, which is
1033
- // neither of those strings.
1066
+ // service.name and cloud.resource_id identify the same deployable, but
1067
+ // instanceName is the template's logical id, a different string from
1068
+ // both.
1034
1069
  semconv: {},
1035
1070
  behavior: {
1036
1071
  /** A process reads its config at startup, and nothing replies. */
@@ -1039,46 +1074,53 @@ var runtimeConfigSemantics = defineBoundarySemantics({
1039
1074
  // comes from outside the code.
1040
1075
  leavesTheProcess: true,
1041
1076
  reportsUnpairedItself: false,
1042
- identityKey: () => null
1077
+ identityKey: () => null,
1078
+ // A reader sees which deployable's environment this is. Code that
1079
+ // reads a variable does not say which deployable runs it, so a read
1080
+ // has no name.
1081
+ displayLabel(semantics) {
1082
+ const instance = semantics.instanceName;
1083
+ return instance === void 0 ? null : `runtime-config:${instance}`;
1084
+ }
1043
1085
  }
1044
1086
  });
1045
1087
 
1046
1088
  // src/semantics/storage.ts
1047
- var import_zod9 = require("zod");
1048
- var StorageSemanticsSchema = import_zod9.z.object({
1049
- name: import_zod9.z.literal("storage"),
1089
+ var import_zod10 = require("zod");
1090
+ var StorageSemanticsSchema = import_zod10.z.object({
1091
+ name: import_zod10.z.literal("storage"),
1050
1092
  /**
1051
- * Which store this is: postgresql, mysql, aws.dynamodb, or our own
1052
- * word for one OpenTelemetry never named, such as s3, or null when
1053
- * the source states one this reader could not settle. Two products'
1054
- * containers can share a name, so this keeps them apart. A null
1055
- * keeps none of them apart, and the pairing pass says what it does
1056
- * with one.
1093
+ * The storage system: an OpenTelemetry value such as `postgresql` or
1094
+ * `aws.dynamodb`, or suss's own word for one the conventions do not
1095
+ * cover, such as `s3`. Null when the reader could not work out which
1096
+ * system the source uses. Two products' containers can share a name,
1097
+ * and this field keeps them apart. The pairing pass decides how to
1098
+ * treat a null.
1057
1099
  */
1058
- storageSystem: import_zod9.z.string().nullable(),
1100
+ storageSystem: import_zod10.z.string().nullable(),
1059
1101
  /**
1060
1102
  * The ORM, schema, or deployment scope. A single-database setup uses
1061
1103
  * `"default"`, and a monorepo with several schemas gives each one its
1062
1104
  * own value.
1063
1105
  */
1064
- scope: import_zod9.z.string(),
1106
+ scope: import_zod10.z.string(),
1065
1107
  /**
1066
- * The table, bucket, collection, or index, as the source declares
1067
- * it, or null when the source states one this reader could not
1068
- * settle. A null container pairs with nothing, rather than with
1069
- * whatever its source text happens to spell. The string is in the
1070
- * boundary-name syntax: a literal, a pattern with `{}` holes, or a
1071
- * reference, and `parseBoundaryName` is the one reader of it.
1108
+ * The table, bucket, collection or index, as the source declares it.
1109
+ * Null when the reader could not work out the container, and a null
1110
+ * container pairs with nothing, so it cannot match by accident of
1111
+ * shared source text. The string uses the boundary-name syntax (a
1112
+ * literal, a pattern with `{}` holes, or a reference), and only
1113
+ * `parseBoundaryName` should read it.
1072
1114
  */
1073
- container: import_zod9.z.string().nullable(),
1115
+ container: import_zod10.z.string().nullable(),
1074
1116
  /**
1075
- * A secondary way into the container, a DynamoDB global secondary
1076
- * index or an Elasticsearch alias, each with its own key fields.
1077
- * Null means the container's own primary way in. A query through an
1117
+ * A secondary way into the container, such as a DynamoDB global
1118
+ * secondary index or an Elasticsearch alias, each with its own key
1119
+ * fields. Null means the container's primary key. A query through an
1078
1120
  * index and a query through the table are different accesses, so
1079
1121
  * they pair separately.
1080
1122
  */
1081
- accessPath: import_zod9.z.string().nullable()
1123
+ accessPath: import_zod10.z.string().nullable()
1082
1124
  });
1083
1125
  function containerReference(semantics) {
1084
1126
  return semantics.container === null ? null : referenceFromName(semantics.container);
@@ -1088,21 +1130,21 @@ var storageSemantics = defineBoundarySemantics({
1088
1130
  schema: StorageSemanticsSchema,
1089
1131
  semconv: {
1090
1132
  storageSystem: { name: "db.system.name" },
1091
- // "default" is our word for a source that named no database.
1133
+ // suss writes "default" when the source did not say which database.
1092
1134
  scope: { name: "db.namespace", placeholderValues: ["default"] },
1093
1135
  container: { name: "db.collection.name" }
1094
- // A secondary index has no attribute of its own, so accessPath
1095
- // stays our word.
1136
+ // The conventions have no attribute for a secondary index, so
1137
+ // accessPath is left out.
1096
1138
  },
1097
1139
  behavior: {
1098
- /** A query returns rows or items, not a status and a body. */
1140
+ /** A query returns rows or items, with no status. */
1099
1141
  exchangesHttpResponses: false,
1100
1142
  leavesTheProcess: true,
1101
1143
  reportsUnpairedItself: false,
1102
1144
  identityKey: () => null,
1103
1145
  displayLabel: storageLabel,
1104
1146
  /**
1105
- * A container the source states as a variable is whatever the
1147
+ * A container the source gives as a variable is whatever the
1106
1148
  * deployment sets that variable to. `{SUBSCRIBERS_TABLE}` in the
1107
1149
  * code and `prod-subscribers-v1` in the manifest are one table.
1108
1150
  */
@@ -1132,9 +1174,9 @@ function storageContainerLabel(semantics) {
1132
1174
  }
1133
1175
 
1134
1176
  // src/semantics/unitInvocation.ts
1135
- var import_zod10 = require("zod");
1177
+ var import_zod11 = require("zod");
1136
1178
  var UnitInvocationSemanticsSchema = DeployableUnitSchema.extend({
1137
- name: import_zod10.z.literal("unit-invocation"),
1179
+ name: import_zod11.z.literal("unit-invocation"),
1138
1180
  instanceName: DeployableUnitSchema.shape.instanceName.nullable()
1139
1181
  });
1140
1182
  function calleeReference(semantics) {
@@ -1164,9 +1206,9 @@ var unitInvocationSemantics = defineBoundarySemantics({
1164
1206
  */
1165
1207
  reportsUnpairedItself: false,
1166
1208
  /**
1167
- * A callee the source states only as a variable is not a name for
1168
- * anything until the deployment fills it in, so it keys as nothing
1169
- * rather than as a bucket that would agree with itself alone.
1209
+ * A callee the source gives only as a variable has no name until the
1210
+ * deployment fills it in. It gets no key, since a key built from the
1211
+ * variable would pair only with itself.
1170
1212
  */
1171
1213
  identityKey(semantics) {
1172
1214
  if (semantics.instanceName === null || namesNothing(semantics.instanceName)) {
@@ -1187,11 +1229,10 @@ var unitInvocationSemantics = defineBoundarySemantics({
1187
1229
  );
1188
1230
  },
1189
1231
  /**
1190
- * A call that reads its callee's name out of the environment
1191
- * reaches whichever resource the template wires that variable to.
1192
- * The logical id is the answer rather than the deployed name,
1193
- * because the invoked unit's own summary is keyed by the logical
1194
- * id and neither side ever writes the other's string.
1232
+ * A call that reads its callee's name from the environment reaches
1233
+ * whichever resource the template wires that variable to. The name
1234
+ * is grounded to the logical id and not the deployed name, because
1235
+ * the invoked unit's own summary is keyed by the logical id.
1195
1236
  */
1196
1237
  groundName(semantics, deployment) {
1197
1238
  const reference = calleeReference(semantics);
@@ -1206,7 +1247,7 @@ var unitInvocationSemantics = defineBoundarySemantics({
1206
1247
  });
1207
1248
 
1208
1249
  // src/semantics/registry.ts
1209
- var SemanticsSchema = import_zod11.z.discriminatedUnion("name", [
1250
+ var SemanticsSchema = import_zod12.z.discriminatedUnion("name", [
1210
1251
  restSemantics.schema,
1211
1252
  functionCallSemantics.schema,
1212
1253
  graphqlResolverSemantics.schema,
@@ -1215,7 +1256,8 @@ var SemanticsSchema = import_zod11.z.discriminatedUnion("name", [
1215
1256
  storageSemantics.schema,
1216
1257
  messageBusSemantics.schema,
1217
1258
  metricSemantics.schema,
1218
- unitInvocationSemantics.schema
1259
+ unitInvocationSemantics.schema,
1260
+ ioSemantics.schema
1219
1261
  ]);
1220
1262
  var DEFINITIONS = [
1221
1263
  restSemantics,
@@ -1226,7 +1268,8 @@ var DEFINITIONS = [
1226
1268
  storageSemantics,
1227
1269
  messageBusSemantics,
1228
1270
  metricSemantics,
1229
- unitInvocationSemantics
1271
+ unitInvocationSemantics,
1272
+ ioSemantics
1230
1273
  ];
1231
1274
  var BY_NAME = new Map(
1232
1275
  DEFINITIONS.map((d) => [d.name, d])
@@ -1249,90 +1292,95 @@ function allBehaviors() {
1249
1292
  }
1250
1293
 
1251
1294
  // src/schemas.ts
1252
- var ConfidenceSourceSchema = import_zod12.z.enum([
1295
+ var ConfidenceSourceSchema = import_zod13.z.enum([
1253
1296
  "inferred_static",
1254
1297
  "inferred_ai",
1255
1298
  "declared",
1256
1299
  "derived"
1257
1300
  ]);
1258
- var ConfidenceLevelSchema = import_zod12.z.enum(["high", "medium", "low"]);
1259
- var CorroborationSchema = import_zod12.z.object({
1260
- outcome: import_zod12.z.enum(["observed", "refuted", "untested"]),
1301
+ var ConfidenceLevelSchema = import_zod13.z.enum(["high", "medium", "low"]);
1302
+ var CorroborationSchema = import_zod13.z.object({
1303
+ outcome: import_zod13.z.enum(["observed", "refuted", "untested"]),
1261
1304
  /** Executions that produced a verdict for this claim. */
1262
- runs: import_zod12.z.number(),
1305
+ runs: import_zod13.z.number(),
1263
1306
  /** Present when refuted: the input and observation that disagreed. */
1264
- counterexample: import_zod12.z.unknown().optional(),
1307
+ counterexample: import_zod13.z.unknown().optional(),
1265
1308
  /** Present when untested: why no verdict was reachable. */
1266
- reason: import_zod12.z.string().optional()
1309
+ reason: import_zod13.z.string().optional()
1267
1310
  });
1268
- var ConfidenceInfoSchema = import_zod12.z.object({
1311
+ var ConfidenceInfoSchema = import_zod13.z.object({
1269
1312
  source: ConfidenceSourceSchema,
1270
1313
  level: ConfidenceLevelSchema,
1271
1314
  corroboration: CorroborationSchema.optional()
1272
1315
  });
1273
- var SourceLocationSchema = import_zod12.z.object({
1274
- file: import_zod12.z.string(),
1316
+ var SourceLocationSchema = import_zod13.z.object({
1317
+ file: import_zod13.z.string(),
1275
1318
  /** Line numbers, for a person reading the summary or an editor link. */
1276
- range: import_zod12.z.object({ start: import_zod12.z.number(), end: import_zod12.z.number() }),
1319
+ range: import_zod13.z.object({ start: import_zod13.z.number(), end: import_zod13.z.number() }),
1277
1320
  /**
1278
- * Character offsets of the unit in its file. Identity and joins
1279
- * measure with these, since two functions can share a line and never
1280
- * an offset range; the line `range` above is the presentation of the
1281
- * same place. Absent on summaries no source position backs, a
1282
- * contract artifact's say.
1321
+ * Character offsets of the unit in its file. Identity and joins use
1322
+ * these, because two functions can share a line but never an offset
1323
+ * range. `range` gives the same place in lines. Absent on a summary
1324
+ * with no source position, such as one read from a contract.
1283
1325
  */
1284
- span: import_zod12.z.object({ start: import_zod12.z.number(), end: import_zod12.z.number() }).optional(),
1285
- exportName: import_zod12.z.string().nullable(),
1326
+ span: import_zod13.z.object({ start: import_zod13.z.number(), end: import_zod13.z.number() }).optional(),
1327
+ exportName: import_zod13.z.string().nullable(),
1286
1328
  /**
1287
- * The project the extract was pointed at, by the name that project
1288
- * calls itself.
1329
+ * The project the extract ran on, by the name the project gives
1330
+ * itself.
1289
1331
  *
1290
1332
  * Paths are relative to wherever the extract ran, so two services in
1291
- * one repository both report `src/handlers.ts`, and merging their
1292
- * summaries puts them on top of each other. This field is what tells
1293
- * them apart, and what a reader groups by.
1333
+ * one repository can both report `src/handlers.ts`, and merging their
1334
+ * summaries would mix them up. This field tells them apart, and a
1335
+ * reader groups by it.
1294
1336
  */
1295
- workspace: import_zod12.z.string().optional()
1337
+ workspace: import_zod13.z.string().optional(),
1338
+ /**
1339
+ * The module of the application the unit belongs to, by the name the
1340
+ * project's `suss.json` gives it. Set only when the project lists its
1341
+ * modules, and then on every unit whose file is under a module's root.
1342
+ */
1343
+ module: import_zod13.z.string().optional()
1296
1344
  });
1297
- var BoundaryBindingSchema = import_zod12.z.object({
1298
- transport: import_zod12.z.string(),
1345
+ var BoundaryBindingSchema = import_zod13.z.object({
1346
+ transport: import_zod13.z.string(),
1299
1347
  semantics: SemanticsSchema,
1300
- recognition: import_zod12.z.string()
1348
+ recognition: import_zod13.z.string()
1301
1349
  });
1302
1350
  function typeDefinitionKey(ref) {
1303
1351
  return ref.def ?? null;
1304
1352
  }
1305
- var TypeShapeSchema = import_zod12.z.lazy(
1306
- () => import_zod12.z.discriminatedUnion("type", [
1307
- import_zod12.z.object({
1308
- type: import_zod12.z.literal("record"),
1309
- properties: import_zod12.z.record(import_zod12.z.string(), TypeShapeSchema),
1310
- spreads: import_zod12.z.array(import_zod12.z.object({ sourceText: import_zod12.z.string() })).optional()
1353
+ var TypeShapeSchema = import_zod13.z.lazy(
1354
+ () => import_zod13.z.discriminatedUnion("type", [
1355
+ import_zod13.z.object({
1356
+ type: import_zod13.z.literal("record"),
1357
+ properties: import_zod13.z.record(import_zod13.z.string(), TypeShapeSchema),
1358
+ spreads: import_zod13.z.array(import_zod13.z.object({ sourceText: import_zod13.z.string() })).optional()
1311
1359
  }),
1312
- import_zod12.z.object({ type: import_zod12.z.literal("dictionary"), values: TypeShapeSchema }),
1313
- import_zod12.z.object({ type: import_zod12.z.literal("array"), items: TypeShapeSchema }),
1314
- import_zod12.z.object({
1315
- type: import_zod12.z.literal("literal"),
1316
- value: import_zod12.z.union([import_zod12.z.string(), import_zod12.z.number(), import_zod12.z.boolean()]),
1317
- raw: import_zod12.z.string().optional()
1360
+ import_zod13.z.object({ type: import_zod13.z.literal("dictionary"), values: TypeShapeSchema }),
1361
+ import_zod13.z.object({ type: import_zod13.z.literal("array"), items: TypeShapeSchema }),
1362
+ import_zod13.z.object({
1363
+ type: import_zod13.z.literal("literal"),
1364
+ value: import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()]),
1365
+ raw: import_zod13.z.string().optional()
1318
1366
  }),
1319
- import_zod12.z.object({ type: import_zod12.z.literal("text") }),
1320
- import_zod12.z.object({ type: import_zod12.z.literal("integer") }),
1321
- import_zod12.z.object({ type: import_zod12.z.literal("number") }),
1322
- import_zod12.z.object({ type: import_zod12.z.literal("boolean") }),
1323
- import_zod12.z.object({ type: import_zod12.z.literal("null") }),
1324
- import_zod12.z.object({ type: import_zod12.z.literal("undefined") }),
1325
- import_zod12.z.object({
1326
- type: import_zod12.z.literal("union"),
1327
- variants: import_zod12.z.array(TypeShapeSchema)
1367
+ import_zod13.z.object({ type: import_zod13.z.literal("text") }),
1368
+ import_zod13.z.object({ type: import_zod13.z.literal("integer") }),
1369
+ import_zod13.z.object({ type: import_zod13.z.literal("number") }),
1370
+ import_zod13.z.object({ type: import_zod13.z.literal("boolean") }),
1371
+ import_zod13.z.object({ type: import_zod13.z.literal("null") }),
1372
+ import_zod13.z.object({ type: import_zod13.z.literal("undefined") }),
1373
+ import_zod13.z.object({
1374
+ type: import_zod13.z.literal("union"),
1375
+ variants: import_zod13.z.array(TypeShapeSchema)
1328
1376
  }),
1329
- import_zod12.z.object({
1330
- type: import_zod12.z.literal("ref"),
1331
- name: import_zod12.z.string(),
1332
- from: import_zod12.z.string().optional(),
1333
- def: import_zod12.z.string().optional()
1377
+ import_zod13.z.object({
1378
+ type: import_zod13.z.literal("ref"),
1379
+ name: import_zod13.z.string(),
1380
+ from: import_zod13.z.string().optional(),
1381
+ def: import_zod13.z.string().optional()
1334
1382
  }),
1335
- import_zod12.z.object({ type: import_zod12.z.literal("unknown") })
1383
+ import_zod13.z.object({ type: import_zod13.z.literal("unknown") })
1336
1384
  ])
1337
1385
  );
1338
1386
  var MAX_DEFINITION_DEPTH = 6;
@@ -1454,6 +1502,9 @@ function boundaryLabel(binding) {
1454
1502
  function displayLabel(binding) {
1455
1503
  return boundaryLabel(binding) ?? `${binding.semantics.name}:${binding.recognition}`;
1456
1504
  }
1505
+ function labelWithDetail(binding, detail) {
1506
+ return `${boundaryLabel(binding) ?? binding.semantics.name} ${detail}`;
1507
+ }
1457
1508
  function normalizeRuleBoundary(raw) {
1458
1509
  const trimmed = raw.trim();
1459
1510
  for (const behavior of allBehaviors()) {
@@ -1617,8 +1668,8 @@ var PLATFORM_INJECTED_ENV_VARS = {
1617
1668
  };
1618
1669
 
1619
1670
  // src/relation.ts
1620
- var import_zod13 = require("zod");
1621
- var RelationSchema = import_zod13.z.enum([
1671
+ var import_zod14 = require("zod");
1672
+ var RelationSchema = import_zod14.z.enum([
1622
1673
  "provides",
1623
1674
  "reads",
1624
1675
  "writes",
@@ -1627,71 +1678,78 @@ var RelationSchema = import_zod13.z.enum([
1627
1678
  var EffectRelationSchema = RelationSchema.exclude(["provides"]);
1628
1679
 
1629
1680
  // src/suppressions.ts
1630
- var import_zod14 = require("zod");
1631
- var SuppressionSideSchema = import_zod14.z.object({
1632
- summary: import_zod14.z.string().optional(),
1633
- transitionId: import_zod14.z.string().optional()
1681
+ var import_zod15 = require("zod");
1682
+ var SuppressionSideSchema = import_zod15.z.object({
1683
+ summary: import_zod15.z.string().optional(),
1684
+ transitionId: import_zod15.z.string().optional()
1634
1685
  }).optional();
1635
- var SuppressionRuleSchema = import_zod14.z.object({
1686
+ var SuppressionRuleSchema = import_zod15.z.object({
1636
1687
  /**
1637
- * The finding kind to match, behavioural or intent. It is an open
1638
- * string here because this package is below both IRs, and the loader
1639
- * is what validates it against the published kind enums.
1688
+ * The finding kind to match, behavioral or intent. The schema accepts
1689
+ * any string because ir-core cannot import either IR's kind enum. The
1690
+ * CLI's loader checks it against both.
1640
1691
  */
1641
- kind: import_zod14.z.string().optional(),
1692
+ kind: import_zod15.z.string().optional(),
1642
1693
  /**
1643
- * Boundary as a human-readable key, e.g. "GET /pet/{petId}" or
1644
- * "fn:@acme/api::getUser". REST keys are normalized via the same
1645
- * path normalizer the checkers use, so `:id` and `{id}` compare
1646
- * equal.
1694
+ * The boundary's human-readable key, such as "GET /pet/{petId}" or
1695
+ * "fn:@acme/api::getUser". A REST key goes through the same path
1696
+ * normalizer the checkers use, so `:id` and `{id}` compare equal.
1647
1697
  */
1648
- boundary: import_zod14.z.string().optional(),
1698
+ boundary: import_zod15.z.string().optional(),
1649
1699
  /**
1650
- * Consumer-side discriminators, the narrowest useful match. These
1651
- * mean something only for behavioural findings: a rule that
1652
- * specifies `consumer` never matches an intent finding, because an
1653
- * intent finding has no consumer side.
1700
+ * Consumer-side discriminators, the narrowest useful match. They
1701
+ * apply only to behavioral findings. A rule that sets `consumer`
1702
+ * never matches an intent finding, because an intent finding has no
1703
+ * consumer side.
1654
1704
  */
1655
1705
  consumer: SuppressionSideSchema,
1656
1706
  /**
1657
- * Provider-side discriminators, the mirror of `consumer`. A finding
1658
- * about a status the provider produces has its transition id on this
1659
- * side, and that id is the only handle narrow enough to pick out
1660
- * that one finding. As with `consumer`, a rule that specifies
1661
- * `provider` never matches an intent finding.
1707
+ * Provider-side discriminators, matching the same fields as
1708
+ * `consumer`. A finding about a status the provider produces has its
1709
+ * transition id on this side, and that id is the only value narrow
1710
+ * enough to pick out that one finding. As with `consumer`, a rule
1711
+ * that sets `provider` never matches an intent finding.
1662
1712
  */
1663
1713
  provider: SuppressionSideSchema,
1664
1714
  /**
1665
- * "narrow", the default, requires kind plus one of boundary,
1666
- * consumer.transitionId, or provider.transitionId, which is enough
1667
- * to target a specific class of finding. "broad" opts in to
1668
- * kind-only or boundary-only matches, which also silence future
1669
- * regressions in that category.
1715
+ * The title of one PRD scenario. A PRD finding's boundary is the
1716
+ * whole PRD, so without this a rule accepts every scenario in it,
1717
+ * including one added later. A rule that sets `scenario` matches
1718
+ * only intent findings about a scenario with that title.
1719
+ */
1720
+ scenario: import_zod15.z.string().optional(),
1721
+ /**
1722
+ * "narrow", the default, requires `kind` plus one of `boundary`,
1723
+ * `scenario`, `consumer.transitionId` or `provider.transitionId`, so
1724
+ * the rule targets a specific class of finding. "broad" allows a
1725
+ * match on kind alone or boundary alone, and such a rule also
1726
+ * silences future regressions in that category.
1670
1727
  */
1671
- scope: import_zod14.z.enum(["narrow", "broad"]).default("narrow"),
1728
+ scope: import_zod15.z.enum(["narrow", "broad"]).default("narrow"),
1672
1729
  /** Required human-written justification. */
1673
- reason: import_zod14.z.string().min(1),
1730
+ reason: import_zod15.z.string().min(1),
1674
1731
  /** What to do when a finding matches. */
1675
- effect: import_zod14.z.enum(["mark", "downgrade", "hide"]).default("mark")
1732
+ effect: import_zod15.z.enum(["mark", "downgrade", "hide"]).default("mark")
1676
1733
  }).strict();
1677
- var SuppressionFileSchema = import_zod14.z.object({
1678
- version: import_zod14.z.literal(1),
1679
- rules: import_zod14.z.array(SuppressionRuleSchema)
1734
+ var SuppressionFileSchema = import_zod15.z.object({
1735
+ version: import_zod15.z.literal(1),
1736
+ rules: import_zod15.z.array(SuppressionRuleSchema)
1680
1737
  }).strict();
1681
1738
  function validateRule(rule) {
1682
1739
  if (rule.scope === "broad") {
1683
- if (rule.kind === void 0 && rule.boundary === void 0 && rule.consumer === void 0 && rule.provider === void 0) {
1684
- return "broad-scope rule must constrain at least one field (kind, boundary, consumer, or provider)";
1740
+ if (rule.kind === void 0 && rule.boundary === void 0 && rule.scenario === void 0 && rule.consumer === void 0 && rule.provider === void 0) {
1741
+ return "broad-scope rule must constrain at least one field (kind, boundary, scenario, consumer, or provider)";
1685
1742
  }
1686
1743
  return null;
1687
1744
  }
1688
1745
  const hasKind = rule.kind !== void 0;
1689
1746
  const hasBoundary = rule.boundary !== void 0;
1747
+ const hasScenario = rule.scenario !== void 0;
1690
1748
  const hasTransition = rule.consumer?.transitionId !== void 0 || rule.provider?.transitionId !== void 0;
1691
- if (hasKind && (hasBoundary || hasTransition)) {
1749
+ if (hasKind && (hasBoundary || hasScenario || hasTransition)) {
1692
1750
  return null;
1693
1751
  }
1694
- return "narrow-scope rule must specify kind AND (boundary OR consumer.transitionId OR provider.transitionId); set scope: 'broad' to silence wider categories";
1752
+ return "narrow-scope rule must specify kind AND (boundary OR scenario OR consumer.transitionId OR provider.transitionId); set scope: 'broad' to silence wider categories";
1695
1753
  }
1696
1754
  function ruleBoundaryMatchesKey(ruleBoundary, key) {
1697
1755
  if (key === null) {
@@ -1743,6 +1801,12 @@ function countsForThreshold(finding) {
1743
1801
  return finding.suppressed.effect === "downgrade";
1744
1802
  }
1745
1803
 
1804
+ // src/testName.ts
1805
+ var TEST_TITLE_SEPARATOR = " > ";
1806
+ function testUnitName(titles) {
1807
+ return titles.join(TEST_TITLE_SEPARATOR);
1808
+ }
1809
+
1746
1810
  // src/typeShapeMatch.ts
1747
1811
  function bodyShapesMatch(actual, declared) {
1748
1812
  if (actual.type === "unknown" || declared.type === "unknown") {
@@ -1930,6 +1994,14 @@ function packageExportBinding(opts) {
1930
1994
  exportPath: opts.exportPath
1931
1995
  });
1932
1996
  }
1997
+ function moduleExportBinding(opts) {
1998
+ return functionCallBinding({
1999
+ transport: "in-process",
2000
+ recognition: opts.recognition,
2001
+ module: opts.module,
2002
+ exportName: opts.exportName
2003
+ });
2004
+ }
1933
2005
  function graphqlResolverBinding(opts) {
1934
2006
  return {
1935
2007
  transport: opts.transport,
@@ -2012,6 +2084,13 @@ function metricBinding(opts) {
2012
2084
  recognition: opts.recognition
2013
2085
  };
2014
2086
  }
2087
+ function ioBinding(opts) {
2088
+ return {
2089
+ transport: "in-process",
2090
+ semantics: { name: "io", target: namedOrNull(opts.target, "io target") },
2091
+ recognition: opts.recognition
2092
+ };
2093
+ }
2015
2094
  // Annotate the CommonJS export names for ESM import in node:
2016
2095
  0 && (module.exports = {
2017
2096
  BoundaryBindingSchema,
@@ -2025,6 +2104,7 @@ function metricBinding(opts) {
2025
2104
  FunctionCallSemanticsSchema,
2026
2105
  GraphqlOperationSemanticsSchema,
2027
2106
  GraphqlResolverSemanticsSchema,
2107
+ IoSemanticsSchema,
2028
2108
  MessageBusSemanticsSchema,
2029
2109
  MetricSemanticsSchema,
2030
2110
  NOTHING_DEPLOYED,
@@ -2037,6 +2117,7 @@ function metricBinding(opts) {
2037
2117
  StorageSemanticsSchema,
2038
2118
  SuppressionFileSchema,
2039
2119
  SuppressionRuleSchema,
2120
+ TEST_TITLE_SEPARATOR,
2040
2121
  TypeShapeSchema,
2041
2122
  UnitInvocationSemanticsSchema,
2042
2123
  applySuppressionsToFindings,
@@ -2070,12 +2151,17 @@ function metricBinding(opts) {
2070
2151
  groundBinding,
2071
2152
  groundedPairingKey,
2072
2153
  hasNameHole,
2154
+ ioBinding,
2155
+ ioIdentityKey,
2073
2156
  isGraphqlOperationBinding,
2157
+ isModuleName,
2158
+ labelWithDetail,
2074
2159
  leavesTheProcess,
2075
2160
  messageBusBinding,
2076
2161
  methodsAgree,
2077
2162
  metricBinding,
2078
2163
  metricIdentityKey,
2164
+ moduleExportBinding,
2079
2165
  namePatternFromSub,
2080
2166
  namePatternKey,
2081
2167
  nameReference,
@@ -2115,6 +2201,7 @@ function metricBinding(opts) {
2115
2201
  storageContainerLabel,
2116
2202
  storageLabel,
2117
2203
  storageSystemLabel,
2204
+ testUnitName,
2118
2205
  typeDefinitionKey,
2119
2206
  unitIdentityKey,
2120
2207
  unitInvocationBinding,