@suss/ir-core 0.3.1 → 0.4.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
@@ -41,23 +41,34 @@ __export(index_exports, {
41
41
  applySuppressionsToFindings: () => applySuppressionsToFindings,
42
42
  bodyShapesMatch: () => bodyShapesMatch,
43
43
  boundaryKey: () => boundaryKey,
44
+ boundaryLabel: () => boundaryLabel,
44
45
  busesAgree: () => busesAgree,
45
46
  channelsPair: () => channelsPair,
46
47
  codeScopePath: () => codeScopePath,
47
48
  countsForThreshold: () => countsForThreshold,
48
49
  dispatchByType: () => dispatchByType,
50
+ displayLabel: () => displayLabel,
51
+ ecsContainerInstanceName: () => ecsContainerInstanceName,
52
+ exchangesHttpResponses: () => exchangesHttpResponses,
49
53
  fileInCodeScope: () => fileInCodeScope,
54
+ formatChannel: () => formatChannel,
50
55
  functionCallBinding: () => functionCallBinding,
51
56
  graphqlOperationBinding: () => graphqlOperationBinding,
52
57
  graphqlResolverBinding: () => graphqlResolverBinding,
53
58
  messageBusBinding: () => messageBusBinding,
59
+ methodsAgree: () => methodsAgree,
54
60
  normalizePath: () => normalizePath,
55
61
  normalizeRuleBoundary: () => normalizeRuleBoundary,
56
62
  packageExportBinding: () => packageExportBinding,
63
+ pairingKey: () => pairingKey,
57
64
  parseChannel: () => parseChannel,
65
+ reportsUnpairedItself: () => reportsUnpairedItself,
58
66
  restBinding: () => restBinding,
67
+ routePathAdmits: () => routePathAdmits,
59
68
  ruleBoundaryMatchesKey: () => ruleBoundaryMatchesKey,
60
69
  runtimeConfigBinding: () => runtimeConfigBinding,
70
+ semanticsAgree: () => semanticsAgree,
71
+ servesRequest: () => servesRequest,
61
72
  storageRelationalBinding: () => storageRelationalBinding,
62
73
  typeDefinitionKey: () => typeDefinitionKey,
63
74
  validateRule: () => validateRule,
@@ -66,56 +77,20 @@ __export(index_exports, {
66
77
  module.exports = __toCommonJS(index_exports);
67
78
 
68
79
  // src/schemas.ts
80
+ var import_zod10 = require("zod");
81
+
82
+ // src/semantics/registry.ts
83
+ var import_zod9 = require("zod");
84
+
85
+ // src/semantics/functionCall.ts
69
86
  var import_zod = require("zod");
70
- var ConfidenceSourceSchema = import_zod.z.enum([
71
- "inferred_static",
72
- "inferred_ai",
73
- "declared",
74
- "derived"
75
- ]);
76
- var ConfidenceLevelSchema = import_zod.z.enum(["high", "medium", "low"]);
77
- var CorroborationSchema = import_zod.z.object({
78
- outcome: import_zod.z.enum(["observed", "refuted", "untested"]),
79
- /** Executions that produced a verdict for this claim. */
80
- runs: import_zod.z.number(),
81
- /** Present when refuted: the input and observation that disagreed. */
82
- counterexample: import_zod.z.unknown().optional(),
83
- /** Present when untested: why no verdict was reachable. */
84
- reason: import_zod.z.string().optional()
85
- });
86
- var ConfidenceInfoSchema = import_zod.z.object({
87
- source: ConfidenceSourceSchema,
88
- level: ConfidenceLevelSchema,
89
- corroboration: CorroborationSchema.optional()
90
- });
91
- var SourceLocationSchema = import_zod.z.object({
92
- file: import_zod.z.string(),
93
- range: import_zod.z.object({ start: import_zod.z.number(), end: import_zod.z.number() }),
94
- exportName: import_zod.z.string().nullable(),
95
- /**
96
- * What the extract was pointed at, as that project calls itself.
97
- *
98
- * A path is relative to wherever the extract ran, so two services in
99
- * one repository both say `src/handlers.ts` and merging their
100
- * summaries puts them on top of each other. This is what tells them
101
- * apart, and what a reader groups by.
102
- */
103
- workspace: import_zod.z.string().optional()
104
- });
105
- var RestSemanticsSchema = import_zod.z.object({
106
- name: import_zod.z.literal("rest"),
107
- /** Uppercase HTTP method ("GET", "POST", …). */
108
- method: import_zod.z.string(),
109
- /** Normalized route path ("/users/{id}"). */
110
- path: import_zod.z.string(),
111
- /**
112
- * Status codes the producing source explicitly declared (OpenAPI
113
- * responses, CFN MethodResponses, ts-rest router statuses). Kept here
114
- * so the pairing layer can still see them without unwrapping metadata.
115
- * Empty / absent for inferred sources.
116
- */
117
- declaredResponses: import_zod.z.array(import_zod.z.number()).optional()
118
- });
87
+
88
+ // src/semantics/definition.ts
89
+ function defineBoundarySemantics(definition) {
90
+ return definition;
91
+ }
92
+
93
+ // src/semantics/functionCall.ts
119
94
  var FunctionCallSemanticsSchema = import_zod.z.object({
120
95
  name: import_zod.z.literal("function-call"),
121
96
  /**
@@ -128,117 +103,503 @@ var FunctionCallSemanticsSchema = import_zod.z.object({
128
103
  /** Named export within the module, when applicable. */
129
104
  exportName: import_zod.z.string().optional(),
130
105
  /**
131
- * Package name (as written in `package.json`) when this identity
132
- * refers to a public package export — e.g. `"@suss/behavioral-ir"`.
133
- * Set alongside `exportPath` by packs that resolve a package's
134
- * public surface (the `packageExports` discovery variant). Distinct
135
- * from `module`, which is a repo-relative module path for
136
- * intra-repo pairing.
106
+ * Package name, as written in `package.json`, when this identity
107
+ * refers to a public package export, for example
108
+ * `"@suss/behavioral-ir"`. Packs that resolve a package's public
109
+ * surface (the `packageExports` discovery variant) set it alongside
110
+ * `exportPath`. It is different from `module`, which is a
111
+ * repo-relative module path used for pairing inside one repo.
137
112
  */
138
113
  package: import_zod.z.string().optional(),
139
114
  /**
140
- * Path to the exported binding within the package, starting with
141
- * the sub-path key when one is used. Examples:
142
- * `["parseSummary"]` — root export
143
- * `["schemas", "BoundaryBindingSchema"]` — sub-path `./schemas`
115
+ * Path to the exported binding within the package, starting with the
116
+ * sub-path key when one is used. Examples:
117
+ * `["parseSummary"]`: a root export
118
+ * `["schemas", "BoundaryBindingSchema"]`: the sub-path `./schemas`
144
119
  *
145
- * The first segment is the sub-path without the leading `./`
146
- * (`"."` → omitted). The last segment is the exported name.
120
+ * The first segment is the sub-path without the leading `./`, and
121
+ * `"."` is left out. The last segment is the exported name.
147
122
  */
148
123
  exportPath: import_zod.z.array(import_zod.z.string()).optional()
149
124
  });
150
- var GraphqlResolverSemanticsSchema = import_zod.z.object({
151
- name: import_zod.z.literal("graphql-resolver"),
152
- /** GraphQL type the resolver attaches to: "Query", "Mutation", "Subscription", or an object-type name. */
153
- typeName: import_zod.z.string(),
125
+ var functionCallSemantics = defineBoundarySemantics({
126
+ name: "function-call",
127
+ schema: FunctionCallSemanticsSchema,
128
+ behavior: {
129
+ /** A call returns a value, which is not a status and a body. */
130
+ exchangesHttpResponses: false,
131
+ reportsUnpairedItself: false,
132
+ /**
133
+ * `"fn:<package>::<exportPath>"` when both are set. Other in-process
134
+ * function-call units, such as components and bare handlers inside
135
+ * one repo, have no key at all.
136
+ */
137
+ identityKey(semantics) {
138
+ if (semantics.package !== void 0 && semantics.exportPath !== void 0 && semantics.exportPath.length > 0) {
139
+ return `fn:${semantics.package}::${semantics.exportPath.join(".")}`;
140
+ }
141
+ return null;
142
+ }
143
+ }
144
+ });
145
+
146
+ // src/semantics/graphqlOperation.ts
147
+ var import_zod2 = require("zod");
148
+ var GraphqlOperationSemanticsSchema = import_zod2.z.object({
149
+ name: import_zod2.z.literal("graphql-operation"),
150
+ /** Optional operation name. An anonymous query or mutation leaves it unset. */
151
+ operationName: import_zod2.z.string().optional(),
152
+ operationType: import_zod2.z.enum(["query", "mutation", "subscription"])
153
+ });
154
+ var graphqlOperationSemantics = defineBoundarySemantics({
155
+ name: "graphql-operation",
156
+ schema: GraphqlOperationSemanticsSchema,
157
+ behavior: {
158
+ /**
159
+ * An operation gets a data-and-errors document back, which the
160
+ * GraphQL contract checker reads. Status codes say nothing here.
161
+ */
162
+ exchangesHttpResponses: false,
163
+ reportsUnpairedItself: false,
164
+ identityKey: () => null,
165
+ /** "query GetUser". The contract checker pairs it, the key does not. */
166
+ displayLabel(semantics) {
167
+ return `${semantics.operationType} ${semantics.operationName ?? "<anonymous>"}`;
168
+ }
169
+ }
170
+ });
171
+
172
+ // src/semantics/graphqlResolver.ts
173
+ var import_zod3 = require("zod");
174
+ var GraphqlResolverSemanticsSchema = import_zod3.z.object({
175
+ name: import_zod3.z.literal("graphql-resolver"),
176
+ /**
177
+ * GraphQL type the resolver attaches to: "Query", "Mutation",
178
+ * "Subscription", or an object-type name. Null when the source never
179
+ * says which one, as with a `@Resolver()` class that has no argument.
180
+ */
181
+ typeName: import_zod3.z.string().min(1).nullable(),
154
182
  /** Field name on that type. */
155
- fieldName: import_zod.z.string()
183
+ fieldName: import_zod3.z.string()
156
184
  });
157
- var GraphqlOperationSemanticsSchema = import_zod.z.object({
158
- name: import_zod.z.literal("graphql-operation"),
159
- /** Optional operation name — anonymous queries / mutations leave this unset. */
160
- operationName: import_zod.z.string().optional(),
161
- operationType: import_zod.z.enum(["query", "mutation", "subscription"])
185
+ var graphqlResolverSemantics = defineBoundarySemantics({
186
+ name: "graphql-resolver",
187
+ schema: GraphqlResolverSemanticsSchema,
188
+ behavior: {
189
+ /** A resolver returns a field value, not a status and a body. */
190
+ exchangesHttpResponses: false,
191
+ reportsUnpairedItself: false,
192
+ /**
193
+ * `"gql:<TypeName>.<fieldName>"`, or null when the type is null or
194
+ * the field is empty, which is how a resolver reports that the
195
+ * source never gave them.
196
+ */
197
+ identityKey(semantics) {
198
+ if (semantics.typeName === null || semantics.fieldName === "") {
199
+ return null;
200
+ }
201
+ return `gql:${semantics.typeName}.${semantics.fieldName}`;
202
+ }
203
+ }
162
204
  });
163
- var DeployableUnitSchema = import_zod.z.object({
164
- deploymentTarget: import_zod.z.enum([
205
+
206
+ // src/semantics/messageBus.ts
207
+ var import_zod4 = require("zod");
208
+
209
+ // src/channel.ts
210
+ function parseChannel(channel) {
211
+ const hash = channel.indexOf("#");
212
+ if (hash === -1) {
213
+ return { bus: null, subject: channel };
214
+ }
215
+ return { bus: channel.slice(0, hash), subject: channel.slice(hash + 1) };
216
+ }
217
+ function formatChannel(bus, subject) {
218
+ if (bus === null) {
219
+ return subject;
220
+ }
221
+ return `${bus}#${subject}`;
222
+ }
223
+ function busesAgree(a, b) {
224
+ return a === null || b === null || a === b;
225
+ }
226
+ function channelsPair(a, b) {
227
+ const left = parseChannel(a);
228
+ const right = parseChannel(b);
229
+ return left.subject === right.subject && busesAgree(left.bus, right.bus);
230
+ }
231
+
232
+ // src/semantics/messageBus.ts
233
+ var MessageBusSemanticsSchema = import_zod4.z.object({
234
+ name: import_zod4.z.literal("message-bus"),
235
+ messageBus: import_zod4.z.enum([
236
+ "sqs",
237
+ "sns",
238
+ "s3",
239
+ "eventbridge",
240
+ "bullmq",
241
+ "kafka",
242
+ "nats"
243
+ ]),
244
+ /**
245
+ * Stable channel identifier: a CFN logical id, a queue or topic name,
246
+ * a subject pattern, or `bus#detailType`. Null when this source does
247
+ * not say which channel, as with a send whose queue the code picks at
248
+ * runtime, or a receive whose queue the event-source mapping states.
249
+ */
250
+ channel: import_zod4.z.string().min(1).nullable()
251
+ });
252
+ var messageBusSemantics = defineBoundarySemantics({
253
+ name: "message-bus",
254
+ schema: MessageBusSemanticsSchema,
255
+ behavior: {
256
+ /** A message goes onto the channel and nothing comes back. */
257
+ exchangesHttpResponses: false,
258
+ /**
259
+ * `checkMessageBus` looks at every channel, including the unused
260
+ * ones, so the generic unmatched lists leave them alone.
261
+ */
262
+ reportsUnpairedItself: true,
263
+ /**
264
+ * `"bus:<messageBus> <subject>"`, or null when the channel is null
265
+ * or its subject is empty.
266
+ *
267
+ * The key has the subject in it and leaves out the bus, so a
268
+ * template writing `default#order.placed` and a handler writing
269
+ * `order.placed` land in one bucket, where `sidesAgree` compares the
270
+ * buses. A side that cannot know its bus would otherwise be keyed
271
+ * away from the side that can.
272
+ *
273
+ * The bus technology stays in, because a queue and an event router
274
+ * are different destinations even with the same subject. Case stays
275
+ * too, because AWS compares detail-types and queue ids byte for byte.
276
+ */
277
+ identityKey(semantics) {
278
+ if (semantics.channel === null) {
279
+ return null;
280
+ }
281
+ const { subject } = parseChannel(semantics.channel);
282
+ if (subject === "") {
283
+ return null;
284
+ }
285
+ return `bus:${semantics.messageBus} ${subject}`;
286
+ },
287
+ sidesAgree(a, b) {
288
+ if (a.channel === null || b.channel === null) {
289
+ return false;
290
+ }
291
+ return channelsPair(a.channel, b.channel);
292
+ },
293
+ /**
294
+ * The whole channel, bus included, unlike the identity key, which
295
+ * has the subject alone. Someone reading a list of unmatched
296
+ * channels wants to see which bus each one was on.
297
+ */
298
+ displayLabel(semantics) {
299
+ if (semantics.channel === null) {
300
+ return `bus:${semantics.messageBus} (channel named at runtime)`;
301
+ }
302
+ return `bus:${semantics.messageBus} ${semantics.channel}`;
303
+ }
304
+ }
305
+ });
306
+
307
+ // src/semantics/rest.ts
308
+ var import_zod5 = require("zod");
309
+ var RestSemanticsSchema = import_zod5.z.object({
310
+ name: import_zod5.z.literal("rest"),
311
+ /**
312
+ * Uppercase HTTP method ("GET", "POST", …), `"*"` for a handler that
313
+ * responds to every method, or null when this source does not say
314
+ * which method.
315
+ */
316
+ method: import_zod5.z.string().min(1).nullable(),
317
+ /** Normalized route path ("/users/{id}"), or null when this source does not give one. */
318
+ path: import_zod5.z.string().min(1).nullable(),
319
+ /**
320
+ * Status codes the producing source explicitly declared (OpenAPI
321
+ * responses, CFN MethodResponses, ts-rest router statuses). They are
322
+ * kept here so the pairing layer can see them without unwrapping any
323
+ * metadata. Absent, or empty, for a source that was inferred.
324
+ */
325
+ declaredResponses: import_zod5.z.array(import_zod5.z.number()).optional()
326
+ });
327
+ function normalizePath(path) {
328
+ let normalized = path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
329
+ if (normalized.length > 1 && normalized.endsWith("/")) {
330
+ normalized = normalized.slice(0, -1);
331
+ }
332
+ normalized = normalized.replace(
333
+ /\{[^}]+\}|[^{]+/g,
334
+ (segment) => segment.startsWith("{") ? segment : segment.toLowerCase()
335
+ );
336
+ return normalized;
337
+ }
338
+ function methodsAgree(a, b) {
339
+ if (a === null || b === null) {
340
+ return false;
341
+ }
342
+ return a === b || a === "*" || b === "*";
343
+ }
344
+ function routePathRegex(pattern) {
345
+ const source = pattern.split(/(\{[^}]+\}|\*)/g).map((part) => {
346
+ if (part === "*") {
347
+ return ".*";
348
+ }
349
+ if (part.startsWith("{") && part.endsWith("}")) {
350
+ return "[^/]+";
351
+ }
352
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
353
+ }).join("");
354
+ return new RegExp(`^${source}$`);
355
+ }
356
+ function routePathAdmits(declaredPath, requestPath) {
357
+ return routePathRegex(normalizePath(declaredPath)).test(
358
+ normalizePath(requestPath)
359
+ );
360
+ }
361
+ var restSemantics = defineBoundarySemantics({
362
+ name: "rest",
363
+ schema: RestSemanticsSchema,
364
+ behavior: {
365
+ /** A request goes out, a status and a body come back. */
366
+ exchangesHttpResponses: true,
367
+ reportsUnpairedItself: false,
368
+ /** `"METHOD /normalized/path"`, or null when either half is missing. */
369
+ identityKey(semantics) {
370
+ if (semantics.method === null || semantics.path === null) {
371
+ return null;
372
+ }
373
+ return `${semantics.method.toUpperCase()} ${normalizePath(semantics.path)}`;
374
+ },
375
+ /**
376
+ * The bucket has only the path in it, so a `"*"` route lands with
377
+ * the consumers that each specify a method, and `methodsAgree`
378
+ * settles it inside the bucket. The identity key stays what a reader
379
+ * sees and a suppression targets ("GET /users", "* /users"), and
380
+ * this key groups what a reader still tells apart.
381
+ */
382
+ pairingKey(semantics) {
383
+ if (semantics.method === null || semantics.path === null) {
384
+ return null;
385
+ }
386
+ return `rest ${normalizePath(semantics.path)}`;
387
+ },
388
+ sidesAgree(a, b) {
389
+ return methodsAgree(a.method, b.method);
390
+ },
391
+ /**
392
+ * What the identity key says, with a missing half still readable:
393
+ * ANY when no method was stated, and ? when no path was. Null when
394
+ * neither was stated, because then there is nothing to show.
395
+ */
396
+ displayLabel(semantics) {
397
+ if (semantics.method === null && semantics.path === null) {
398
+ return null;
399
+ }
400
+ const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
401
+ const path = semantics.path === null ? "?" : normalizePath(semantics.path);
402
+ return `${method} ${path}`;
403
+ },
404
+ /**
405
+ * A route with both halves stated decides for itself: the method
406
+ * through `methodsAgree` (so a `"*"` route matches every method),
407
+ * and the path through `routePathAdmits`. A route missing either
408
+ * half might still be the one that handles the request, and nothing
409
+ * here can settle that, so it abstains instead of refusing.
410
+ */
411
+ servesRequest(semantics, method, path) {
412
+ if (semantics.method === null || semantics.path === null) {
413
+ return "unknown";
414
+ }
415
+ if (!methodsAgree(semantics.method, method.toUpperCase())) {
416
+ return "nomatch";
417
+ }
418
+ return routePathAdmits(semantics.path, path) ? "match" : "nomatch";
419
+ },
420
+ ruleBoundary: {
421
+ // "METHOD /path": one leading token, then an absolute path.
422
+ claims(raw) {
423
+ return /^\S+ +\//.test(raw.trim());
424
+ },
425
+ normalize(raw) {
426
+ const trimmed = raw.trim();
427
+ const spaceIdx = trimmed.indexOf(" ");
428
+ const method = trimmed.slice(0, spaceIdx).toUpperCase();
429
+ return `${method} ${normalizePath(trimmed.slice(spaceIdx + 1).trim())}`;
430
+ }
431
+ }
432
+ }
433
+ });
434
+
435
+ // src/semantics/runtimeConfig.ts
436
+ var import_zod7 = require("zod");
437
+
438
+ // src/deployableUnit.ts
439
+ var import_zod6 = require("zod");
440
+ var DeployableUnitSchema = import_zod6.z.object({
441
+ deploymentTarget: import_zod6.z.enum([
165
442
  "lambda",
166
443
  "ecs-task",
167
444
  "container",
168
445
  "k8s-deployment"
169
446
  ]),
170
447
  // An empty name would agree with every other empty name, so a unit
171
- // that names nothing has to leave the field off instead.
172
- instanceName: import_zod.z.string().min(1)
448
+ // with no name has to leave the field off instead.
449
+ instanceName: import_zod6.z.string().min(1)
173
450
  });
451
+ function ecsContainerInstanceName(taskDefinitionLogicalId, containerName) {
452
+ return `${taskDefinitionLogicalId}/${containerName}`;
453
+ }
454
+
455
+ // src/semantics/runtimeConfig.ts
174
456
  var RuntimeConfigSemanticsSchema = DeployableUnitSchema.extend({
175
- name: import_zod.z.literal("runtime-config")
457
+ name: import_zod7.z.literal("runtime-config")
458
+ });
459
+ var runtimeConfigSemantics = defineBoundarySemantics({
460
+ name: "runtime-config",
461
+ schema: RuntimeConfigSemanticsSchema,
462
+ behavior: {
463
+ /** A process reads its config at startup, and nothing replies. */
464
+ exchangesHttpResponses: false,
465
+ reportsUnpairedItself: false,
466
+ identityKey: () => null
467
+ }
176
468
  });
177
- var StorageRelationalSemanticsSchema = import_zod.z.object({
178
- name: import_zod.z.literal("storage-relational"),
179
- storageSystem: import_zod.z.enum(["postgres", "mysql", "sqlite"]),
469
+
470
+ // src/semantics/storageRelational.ts
471
+ var import_zod8 = require("zod");
472
+ var StorageRelationalSemanticsSchema = import_zod8.z.object({
473
+ name: import_zod8.z.literal("storage-relational"),
474
+ storageSystem: import_zod8.z.enum(["postgres", "mysql", "sqlite"]),
180
475
  /**
181
- * ORM / driver scope. Defaults to `"default"` for single-database
182
- * setups; monorepos with multiple schemas use distinct values.
476
+ * The ORM or driver scope. A single-database setup uses `"default"`,
477
+ * and a monorepo with several schemas gives each one its own value.
183
478
  */
184
- scope: import_zod.z.string(),
185
- /** Table / model name as declared in the schema. */
186
- table: import_zod.z.string()
479
+ scope: import_zod8.z.string(),
480
+ /** The table or model name, as declared in the schema. */
481
+ table: import_zod8.z.string()
187
482
  });
188
- var MessageBusSemanticsSchema = import_zod.z.object({
189
- name: import_zod.z.literal("message-bus"),
190
- messageBus: import_zod.z.enum(["sqs", "eventbridge", "bullmq", "kafka", "nats"]),
191
- /** Stable channel identifier — CFN logical id, queue/topic name, subject pattern, `bus#detailType`. */
192
- channel: import_zod.z.string()
483
+ var storageRelationalSemantics = defineBoundarySemantics({
484
+ name: "storage-relational",
485
+ schema: StorageRelationalSemanticsSchema,
486
+ behavior: {
487
+ /** A query returns rows, not a status and a body. */
488
+ exchangesHttpResponses: false,
489
+ reportsUnpairedItself: false,
490
+ identityKey: () => null
491
+ }
193
492
  });
194
- var SemanticsSchema = import_zod.z.discriminatedUnion("name", [
195
- RestSemanticsSchema,
196
- FunctionCallSemanticsSchema,
197
- GraphqlResolverSemanticsSchema,
198
- GraphqlOperationSemanticsSchema,
199
- RuntimeConfigSemanticsSchema,
200
- StorageRelationalSemanticsSchema,
201
- MessageBusSemanticsSchema
493
+
494
+ // src/semantics/registry.ts
495
+ var SemanticsSchema = import_zod9.z.discriminatedUnion("name", [
496
+ restSemantics.schema,
497
+ functionCallSemantics.schema,
498
+ graphqlResolverSemantics.schema,
499
+ graphqlOperationSemantics.schema,
500
+ runtimeConfigSemantics.schema,
501
+ storageRelationalSemantics.schema,
502
+ messageBusSemantics.schema
503
+ ]);
504
+ var DEFINITIONS = [
505
+ restSemantics,
506
+ functionCallSemantics,
507
+ graphqlResolverSemantics,
508
+ graphqlOperationSemantics,
509
+ runtimeConfigSemantics,
510
+ storageRelationalSemantics,
511
+ messageBusSemantics
512
+ ];
513
+ var BY_NAME = new Map(
514
+ DEFINITIONS.map((d) => [d.name, d])
515
+ );
516
+ function behaviorOf(semantics) {
517
+ const definition = BY_NAME.get(semantics.name);
518
+ if (definition === void 0) {
519
+ throw new Error(`no boundary behavior for semantics "${semantics.name}"`);
520
+ }
521
+ return definition.behavior;
522
+ }
523
+ function allBehaviors() {
524
+ return DEFINITIONS.map((d) => d.behavior);
525
+ }
526
+
527
+ // src/schemas.ts
528
+ var ConfidenceSourceSchema = import_zod10.z.enum([
529
+ "inferred_static",
530
+ "inferred_ai",
531
+ "declared",
532
+ "derived"
202
533
  ]);
203
- var BoundaryBindingSchema = import_zod.z.object({
204
- transport: import_zod.z.string(),
534
+ var ConfidenceLevelSchema = import_zod10.z.enum(["high", "medium", "low"]);
535
+ var CorroborationSchema = import_zod10.z.object({
536
+ outcome: import_zod10.z.enum(["observed", "refuted", "untested"]),
537
+ /** Executions that produced a verdict for this claim. */
538
+ runs: import_zod10.z.number(),
539
+ /** Present when refuted: the input and observation that disagreed. */
540
+ counterexample: import_zod10.z.unknown().optional(),
541
+ /** Present when untested: why no verdict was reachable. */
542
+ reason: import_zod10.z.string().optional()
543
+ });
544
+ var ConfidenceInfoSchema = import_zod10.z.object({
545
+ source: ConfidenceSourceSchema,
546
+ level: ConfidenceLevelSchema,
547
+ corroboration: CorroborationSchema.optional()
548
+ });
549
+ var SourceLocationSchema = import_zod10.z.object({
550
+ file: import_zod10.z.string(),
551
+ range: import_zod10.z.object({ start: import_zod10.z.number(), end: import_zod10.z.number() }),
552
+ exportName: import_zod10.z.string().nullable(),
553
+ /**
554
+ * The project the extract was pointed at, by the name that project
555
+ * calls itself.
556
+ *
557
+ * Paths are relative to wherever the extract ran, so two services in
558
+ * one repository both report `src/handlers.ts`, and merging their
559
+ * summaries puts them on top of each other. This field is what tells
560
+ * them apart, and what a reader groups by.
561
+ */
562
+ workspace: import_zod10.z.string().optional()
563
+ });
564
+ var BoundaryBindingSchema = import_zod10.z.object({
565
+ transport: import_zod10.z.string(),
205
566
  semantics: SemanticsSchema,
206
- recognition: import_zod.z.string()
567
+ recognition: import_zod10.z.string()
207
568
  });
208
569
  function typeDefinitionKey(ref) {
209
570
  return ref.def ?? null;
210
571
  }
211
- var TypeShapeSchema = import_zod.z.lazy(
212
- () => import_zod.z.discriminatedUnion("type", [
213
- import_zod.z.object({
214
- type: import_zod.z.literal("record"),
215
- properties: import_zod.z.record(import_zod.z.string(), TypeShapeSchema),
216
- spreads: import_zod.z.array(import_zod.z.object({ sourceText: import_zod.z.string() })).optional()
572
+ var TypeShapeSchema = import_zod10.z.lazy(
573
+ () => import_zod10.z.discriminatedUnion("type", [
574
+ import_zod10.z.object({
575
+ type: import_zod10.z.literal("record"),
576
+ properties: import_zod10.z.record(import_zod10.z.string(), TypeShapeSchema),
577
+ spreads: import_zod10.z.array(import_zod10.z.object({ sourceText: import_zod10.z.string() })).optional()
217
578
  }),
218
- import_zod.z.object({ type: import_zod.z.literal("dictionary"), values: TypeShapeSchema }),
219
- import_zod.z.object({ type: import_zod.z.literal("array"), items: TypeShapeSchema }),
220
- import_zod.z.object({
221
- type: import_zod.z.literal("literal"),
222
- value: import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean()]),
223
- raw: import_zod.z.string().optional()
579
+ import_zod10.z.object({ type: import_zod10.z.literal("dictionary"), values: TypeShapeSchema }),
580
+ import_zod10.z.object({ type: import_zod10.z.literal("array"), items: TypeShapeSchema }),
581
+ import_zod10.z.object({
582
+ type: import_zod10.z.literal("literal"),
583
+ value: import_zod10.z.union([import_zod10.z.string(), import_zod10.z.number(), import_zod10.z.boolean()]),
584
+ raw: import_zod10.z.string().optional()
224
585
  }),
225
- import_zod.z.object({ type: import_zod.z.literal("text") }),
226
- import_zod.z.object({ type: import_zod.z.literal("integer") }),
227
- import_zod.z.object({ type: import_zod.z.literal("number") }),
228
- import_zod.z.object({ type: import_zod.z.literal("boolean") }),
229
- import_zod.z.object({ type: import_zod.z.literal("null") }),
230
- import_zod.z.object({ type: import_zod.z.literal("undefined") }),
231
- import_zod.z.object({
232
- type: import_zod.z.literal("union"),
233
- variants: import_zod.z.array(TypeShapeSchema)
586
+ import_zod10.z.object({ type: import_zod10.z.literal("text") }),
587
+ import_zod10.z.object({ type: import_zod10.z.literal("integer") }),
588
+ import_zod10.z.object({ type: import_zod10.z.literal("number") }),
589
+ import_zod10.z.object({ type: import_zod10.z.literal("boolean") }),
590
+ import_zod10.z.object({ type: import_zod10.z.literal("null") }),
591
+ import_zod10.z.object({ type: import_zod10.z.literal("undefined") }),
592
+ import_zod10.z.object({
593
+ type: import_zod10.z.literal("union"),
594
+ variants: import_zod10.z.array(TypeShapeSchema)
234
595
  }),
235
- import_zod.z.object({
236
- type: import_zod.z.literal("ref"),
237
- name: import_zod.z.string(),
238
- from: import_zod.z.string().optional(),
239
- def: import_zod.z.string().optional()
596
+ import_zod10.z.object({
597
+ type: import_zod10.z.literal("ref"),
598
+ name: import_zod10.z.string(),
599
+ from: import_zod10.z.string().optional(),
600
+ def: import_zod10.z.string().optional()
240
601
  }),
241
- import_zod.z.object({ type: import_zod.z.literal("unknown") })
602
+ import_zod10.z.object({ type: import_zod10.z.literal("unknown") })
242
603
  ])
243
604
  );
244
605
  var MAX_DEFINITION_DEPTH = 6;
@@ -282,65 +643,49 @@ function withDefinitionsInlined(shape, definitions, depth = 0, inProgress = /* @
282
643
  return shape;
283
644
  }
284
645
 
285
- // src/channel.ts
286
- function parseChannel(channel) {
287
- const hash = channel.indexOf("#");
288
- if (hash === -1) {
289
- return { bus: null, subject: channel };
290
- }
291
- return { bus: channel.slice(0, hash), subject: channel.slice(hash + 1) };
292
- }
293
- function busesAgree(a, b) {
294
- return a === null || b === null || a === b;
646
+ // src/boundaryKey.ts
647
+ function boundaryKey(binding) {
648
+ return behaviorOf(binding.semantics).identityKey(binding.semantics);
295
649
  }
296
- function channelsPair(a, b) {
297
- const left = parseChannel(a);
298
- const right = parseChannel(b);
299
- return left.subject === right.subject && busesAgree(left.bus, right.bus);
650
+ function pairingKey(binding) {
651
+ const behavior = behaviorOf(binding.semantics);
652
+ return (behavior.pairingKey ?? behavior.identityKey)(binding.semantics);
300
653
  }
301
-
302
- // src/boundaryKey.ts
303
- function normalizePath(path) {
304
- let normalized = path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
305
- if (normalized.length > 1 && normalized.endsWith("/")) {
306
- normalized = normalized.slice(0, -1);
654
+ function semanticsAgree(a, b) {
655
+ if (a.name !== b.name) {
656
+ return false;
307
657
  }
308
- normalized = normalized.replace(
309
- /\{[^}]+\}|[^{]+/g,
310
- (segment) => segment.startsWith("{") ? segment : segment.toLowerCase()
311
- );
312
- return normalized;
658
+ const agree = behaviorOf(a).sidesAgree;
659
+ return agree === void 0 ? true : agree(a, b);
313
660
  }
314
- function boundaryKey(binding) {
315
- const semantics = binding.semantics;
316
- if (semantics.name === "rest") {
317
- if (semantics.method === "" || semantics.path === "") {
318
- return null;
319
- }
320
- const method = semantics.method.toUpperCase();
321
- const path = normalizePath(semantics.path);
322
- return `${method} ${path}`;
323
- }
324
- if (semantics.name === "graphql-resolver") {
325
- if (semantics.typeName === "" || semantics.fieldName === "") {
326
- return null;
327
- }
328
- return `gql:${semantics.typeName}.${semantics.fieldName}`;
329
- }
330
- if (semantics.name === "message-bus") {
331
- const { subject } = parseChannel(semantics.channel);
332
- if (subject === "") {
333
- return null;
661
+ function boundaryLabel(binding) {
662
+ const behavior = behaviorOf(binding.semantics);
663
+ return (behavior.displayLabel ?? behavior.identityKey)(binding.semantics);
664
+ }
665
+ function displayLabel(binding) {
666
+ return boundaryLabel(binding) ?? `${binding.semantics.name}:${binding.recognition}`;
667
+ }
668
+ function normalizeRuleBoundary(raw) {
669
+ const trimmed = raw.trim();
670
+ for (const behavior of allBehaviors()) {
671
+ if (behavior.ruleBoundary?.claims(trimmed) === true) {
672
+ return behavior.ruleBoundary.normalize(trimmed);
334
673
  }
335
- return `bus:${semantics.messageBus} ${subject}`;
336
674
  }
337
- if (semantics.name === "function-call") {
338
- if (semantics.package !== void 0 && semantics.exportPath !== void 0 && semantics.exportPath.length > 0) {
339
- return `fn:${semantics.package}::${semantics.exportPath.join(".")}`;
340
- }
675
+ return trimmed;
676
+ }
677
+ function exchangesHttpResponses(binding) {
678
+ return behaviorOf(binding.semantics).exchangesHttpResponses;
679
+ }
680
+ function reportsUnpairedItself(binding) {
681
+ return behaviorOf(binding.semantics).reportsUnpairedItself;
682
+ }
683
+ function servesRequest(binding, method, path) {
684
+ const serves = behaviorOf(binding.semantics).servesRequest;
685
+ if (serves === void 0) {
341
686
  return null;
342
687
  }
343
- return null;
688
+ return serves(binding.semantics, method, path);
344
689
  }
345
690
 
346
691
  // src/codeScope.ts
@@ -363,56 +708,56 @@ function dispatchByType(table, value) {
363
708
  }
364
709
 
365
710
  // src/suppressions.ts
366
- var import_zod2 = require("zod");
367
- var SuppressionSideSchema = import_zod2.z.object({
368
- summary: import_zod2.z.string().optional(),
369
- transitionId: import_zod2.z.string().optional()
711
+ var import_zod11 = require("zod");
712
+ var SuppressionSideSchema = import_zod11.z.object({
713
+ summary: import_zod11.z.string().optional(),
714
+ transitionId: import_zod11.z.string().optional()
370
715
  }).optional();
371
- var SuppressionRuleSchema = import_zod2.z.object({
716
+ var SuppressionRuleSchema = import_zod11.z.object({
372
717
  /**
373
- * Finding kind to match — any behavioural or intent finding kind.
374
- * Open here (this package sits below both IRs); the loader
375
- * validates against the published kind enums.
718
+ * The finding kind to match, behavioural or intent. It is an open
719
+ * string here because this package is below both IRs, and the loader
720
+ * is what validates it against the published kind enums.
376
721
  */
377
- kind: import_zod2.z.string().optional(),
722
+ kind: import_zod11.z.string().optional(),
378
723
  /**
379
724
  * Boundary as a human-readable key, e.g. "GET /pet/{petId}" or
380
725
  * "fn:@acme/api::getUser". REST keys are normalized via the same
381
726
  * path normalizer the checkers use, so `:id` and `{id}` compare
382
727
  * equal.
383
728
  */
384
- boundary: import_zod2.z.string().optional(),
729
+ boundary: import_zod11.z.string().optional(),
385
730
  /**
386
- * Consumer-side discriminators (narrowest useful match). Only
387
- * meaningful for behavioural findings — a rule that specifies
388
- * `consumer` never matches an intent finding, which has no
389
- * consumer side.
731
+ * Consumer-side discriminators, the narrowest useful match. These
732
+ * mean something only for behavioural findings: a rule that
733
+ * specifies `consumer` never matches an intent finding, because an
734
+ * intent finding has no consumer side.
390
735
  */
391
736
  consumer: SuppressionSideSchema,
392
737
  /**
393
738
  * Provider-side discriminators, the mirror of `consumer`. A finding
394
- * about a status the provider produces carries its transition id on
395
- * this side, and that id is the only handle narrow enough to name
396
- * that one finding. Like `consumer`, a rule that specifies
739
+ * about a status the provider produces has its transition id on this
740
+ * side, and that id is the only handle narrow enough to pick out
741
+ * that one finding. As with `consumer`, a rule that specifies
397
742
  * `provider` never matches an intent finding.
398
743
  */
399
744
  provider: SuppressionSideSchema,
400
745
  /**
401
- * "narrow" (default): requires kind plus one of boundary,
402
- * consumer.transitionId, or provider.transitionId — enough to
403
- * target a specific finding class. "broad" opts in to kind-only or
404
- * boundary-only matches, which silence future regressions in that
405
- * category too.
746
+ * "narrow", the default, requires kind plus one of boundary,
747
+ * consumer.transitionId, or provider.transitionId, which is enough
748
+ * to target a specific class of finding. "broad" opts in to
749
+ * kind-only or boundary-only matches, which also silence future
750
+ * regressions in that category.
406
751
  */
407
- scope: import_zod2.z.enum(["narrow", "broad"]).default("narrow"),
752
+ scope: import_zod11.z.enum(["narrow", "broad"]).default("narrow"),
408
753
  /** Required human-written justification. */
409
- reason: import_zod2.z.string().min(1),
754
+ reason: import_zod11.z.string().min(1),
410
755
  /** What to do when a finding matches. */
411
- effect: import_zod2.z.enum(["mark", "downgrade", "hide"]).default("mark")
756
+ effect: import_zod11.z.enum(["mark", "downgrade", "hide"]).default("mark")
412
757
  }).strict();
413
- var SuppressionFileSchema = import_zod2.z.object({
414
- version: import_zod2.z.literal(1),
415
- rules: import_zod2.z.array(SuppressionRuleSchema)
758
+ var SuppressionFileSchema = import_zod11.z.object({
759
+ version: import_zod11.z.literal(1),
760
+ rules: import_zod11.z.array(SuppressionRuleSchema)
416
761
  }).strict();
417
762
  function validateRule(rule) {
418
763
  if (rule.scope === "broad") {
@@ -429,16 +774,6 @@ function validateRule(rule) {
429
774
  }
430
775
  return "narrow-scope rule must specify kind AND (boundary OR consumer.transitionId OR provider.transitionId); set scope: 'broad' to silence wider categories";
431
776
  }
432
- function normalizeRuleBoundary(raw) {
433
- const trimmed = raw.trim();
434
- const spaceIdx = trimmed.indexOf(" ");
435
- if (spaceIdx < 0) {
436
- return trimmed;
437
- }
438
- const method = trimmed.slice(0, spaceIdx).toUpperCase();
439
- const path = trimmed.slice(spaceIdx + 1);
440
- return `${method} ${normalizePath(path)}`;
441
- }
442
777
  function ruleBoundaryMatchesKey(ruleBoundary, key) {
443
778
  if (key === null) {
444
779
  return false;
@@ -634,13 +969,22 @@ function allowsUndefined(shape) {
634
969
  }
635
970
 
636
971
  // src/index.ts
972
+ function namedOrNull(value, field) {
973
+ if (value === "") {
974
+ throw new Error(
975
+ `${field} is an empty string; write null when the source does not name it`
976
+ );
977
+ }
978
+ return value;
979
+ }
637
980
  function restBinding(opts) {
981
+ const method = namedOrNull(opts.method, "rest method");
638
982
  return {
639
983
  transport: opts.transport,
640
984
  semantics: {
641
985
  name: "rest",
642
- method: opts.method.toUpperCase(),
643
- path: opts.path,
986
+ method: method === null ? null : method.toUpperCase(),
987
+ path: namedOrNull(opts.path, "rest path"),
644
988
  ...opts.declaredResponses !== void 0 ? { declaredResponses: opts.declaredResponses } : {}
645
989
  },
646
990
  recognition: opts.recognition
@@ -672,7 +1016,7 @@ function graphqlResolverBinding(opts) {
672
1016
  transport: opts.transport,
673
1017
  semantics: {
674
1018
  name: "graphql-resolver",
675
- typeName: opts.typeName,
1019
+ typeName: namedOrNull(opts.typeName, "resolver typeName"),
676
1020
  fieldName: opts.fieldName
677
1021
  },
678
1022
  recognition: opts.recognition
@@ -718,7 +1062,7 @@ function messageBusBinding(opts) {
718
1062
  semantics: {
719
1063
  name: "message-bus",
720
1064
  messageBus: opts.messageBus,
721
- channel: opts.channel
1065
+ channel: namedOrNull(opts.channel, "message-bus channel")
722
1066
  },
723
1067
  recognition: opts.recognition
724
1068
  };
@@ -746,23 +1090,34 @@ function messageBusBinding(opts) {
746
1090
  applySuppressionsToFindings,
747
1091
  bodyShapesMatch,
748
1092
  boundaryKey,
1093
+ boundaryLabel,
749
1094
  busesAgree,
750
1095
  channelsPair,
751
1096
  codeScopePath,
752
1097
  countsForThreshold,
753
1098
  dispatchByType,
1099
+ displayLabel,
1100
+ ecsContainerInstanceName,
1101
+ exchangesHttpResponses,
754
1102
  fileInCodeScope,
1103
+ formatChannel,
755
1104
  functionCallBinding,
756
1105
  graphqlOperationBinding,
757
1106
  graphqlResolverBinding,
758
1107
  messageBusBinding,
1108
+ methodsAgree,
759
1109
  normalizePath,
760
1110
  normalizeRuleBoundary,
761
1111
  packageExportBinding,
1112
+ pairingKey,
762
1113
  parseChannel,
1114
+ reportsUnpairedItself,
763
1115
  restBinding,
1116
+ routePathAdmits,
764
1117
  ruleBoundaryMatchesKey,
765
1118
  runtimeConfigBinding,
1119
+ semanticsAgree,
1120
+ servesRequest,
766
1121
  storageRelationalBinding,
767
1122
  typeDefinitionKey,
768
1123
  validateRule,