@suss/ir-core 0.13.2 → 0.14.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.
@@ -309,6 +309,20 @@ var metricSemantics = defineBoundarySemantics({
309
309
 
310
310
  // src/semantics/rest.ts
311
311
  import { z as z6 } from "zod";
312
+
313
+ // src/urlPath.ts
314
+ var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
315
+ var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
316
+ function pathAfterOrigin(text) {
317
+ const withoutOrigin = text.replace(SCHEME_ORIGIN, "").replace(PROTOCOL_RELATIVE_ORIGIN, "");
318
+ const cut = withoutOrigin.search(/[?#]/);
319
+ return cut === -1 ? withoutOrigin : withoutOrigin.slice(0, cut);
320
+ }
321
+ function statesAnOrigin(text) {
322
+ return SCHEME_ORIGIN.test(text) || PROTOCOL_RELATIVE_ORIGIN.test(text);
323
+ }
324
+
325
+ // src/semantics/rest.ts
312
326
  var RestSemanticsSchema = z6.object({
313
327
  name: z6.literal("rest"),
314
328
  /**
@@ -327,6 +341,11 @@ var RestSemanticsSchema = z6.object({
327
341
  */
328
342
  declaredResponses: z6.array(z6.number()).optional()
329
343
  });
344
+ var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
345
+ function variableOf(label) {
346
+ const parts = label.split(".");
347
+ return parts[parts.length - 1] ?? label;
348
+ }
330
349
  function normalizePath(path) {
331
350
  let normalized = path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
332
351
  if (normalized.length > 1 && normalized.endsWith("/")) {
@@ -436,6 +455,37 @@ var restSemantics = defineBoundarySemantics({
436
455
  }
437
456
  return routePathAdmits(semantics.path, path) ? "match" : "nomatch";
438
457
  },
458
+ /**
459
+ * A call whose base URL the deployment fills in, resolved to the
460
+ * path it reaches.
461
+ *
462
+ * The source cannot settle this on its own, which is why the
463
+ * adapter leaves the hole in. `API_BASE` could be
464
+ * `http://backend.internal`, and then the path is `/orders`. It
465
+ * could equally be `/api/v2`, and then the path is
466
+ * `/api/v2/orders`. Putting the deployed value in and reading the
467
+ * path back out gets the right one either way.
468
+ *
469
+ * Only a hole at the front is a base URL. A hole further along is a
470
+ * route parameter, and `/orders/{id}` means every id rather than a
471
+ * variable somebody set.
472
+ */
473
+ groundName(semantics, deployedAs) {
474
+ if (semantics.path === null) {
475
+ return null;
476
+ }
477
+ const opening = OPENING_HOLE.exec(semantics.path);
478
+ if (opening === null) {
479
+ return null;
480
+ }
481
+ const [, label, rest] = opening;
482
+ const base = deployedAs(variableOf(label));
483
+ if (base === null) {
484
+ return null;
485
+ }
486
+ const grounded = pathAfterOrigin(`${base}${rest}`);
487
+ return { ...semantics, path: grounded === "" ? "/" : grounded };
488
+ },
439
489
  ruleBoundary: {
440
490
  // "METHOD /path": one leading token, then an absolute path.
441
491
  claims(raw) {
@@ -718,6 +768,8 @@ export {
718
768
  channelsPair,
719
769
  MessageBusSemanticsSchema,
720
770
  MetricSemanticsSchema,
771
+ pathAfterOrigin,
772
+ statesAnOrigin,
721
773
  RestSemanticsSchema,
722
774
  normalizePath,
723
775
  pathShape,
package/dist/index.cjs CHANGED
@@ -62,6 +62,7 @@ __export(index_exports, {
62
62
  gqlIdentityKey: () => gqlIdentityKey,
63
63
  graphqlOperationBinding: () => graphqlOperationBinding,
64
64
  graphqlResolverBinding: () => graphqlResolverBinding,
65
+ groundedPairingKey: () => groundedPairingKey,
65
66
  hasNameHole: () => hasNameHole,
66
67
  isGraphqlOperationBinding: () => isGraphqlOperationBinding,
67
68
  messageBusBinding: () => messageBusBinding,
@@ -78,6 +79,7 @@ __export(index_exports, {
78
79
  pairingKey: () => pairingKey,
79
80
  parseBoundaryName: () => parseBoundaryName,
80
81
  parseChannel: () => parseChannel,
82
+ pathAfterOrigin: () => pathAfterOrigin,
81
83
  pathShape: () => pathShape,
82
84
  patternHole: () => patternHole,
83
85
  referenceFromName: () => referenceFromName,
@@ -91,6 +93,7 @@ __export(index_exports, {
91
93
  semanticsAgree: () => semanticsAgree,
92
94
  semconvAttributes: () => semconvAttributes,
93
95
  servesRequest: () => servesRequest,
96
+ statesAnOrigin: () => statesAnOrigin,
94
97
  storageBinding: () => storageBinding,
95
98
  typeDefinitionKey: () => typeDefinitionKey,
96
99
  validateRule: () => validateRule,
@@ -409,6 +412,20 @@ var metricSemantics = defineBoundarySemantics({
409
412
 
410
413
  // src/semantics/rest.ts
411
414
  var import_zod6 = require("zod");
415
+
416
+ // src/urlPath.ts
417
+ var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
418
+ var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
419
+ function pathAfterOrigin(text) {
420
+ const withoutOrigin = text.replace(SCHEME_ORIGIN, "").replace(PROTOCOL_RELATIVE_ORIGIN, "");
421
+ const cut = withoutOrigin.search(/[?#]/);
422
+ return cut === -1 ? withoutOrigin : withoutOrigin.slice(0, cut);
423
+ }
424
+ function statesAnOrigin(text) {
425
+ return SCHEME_ORIGIN.test(text) || PROTOCOL_RELATIVE_ORIGIN.test(text);
426
+ }
427
+
428
+ // src/semantics/rest.ts
412
429
  var RestSemanticsSchema = import_zod6.z.object({
413
430
  name: import_zod6.z.literal("rest"),
414
431
  /**
@@ -427,6 +444,11 @@ var RestSemanticsSchema = import_zod6.z.object({
427
444
  */
428
445
  declaredResponses: import_zod6.z.array(import_zod6.z.number()).optional()
429
446
  });
447
+ var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
448
+ function variableOf(label) {
449
+ const parts = label.split(".");
450
+ return parts[parts.length - 1] ?? label;
451
+ }
430
452
  function normalizePath(path) {
431
453
  let normalized = path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
432
454
  if (normalized.length > 1 && normalized.endsWith("/")) {
@@ -536,6 +558,37 @@ var restSemantics = defineBoundarySemantics({
536
558
  }
537
559
  return routePathAdmits(semantics.path, path) ? "match" : "nomatch";
538
560
  },
561
+ /**
562
+ * A call whose base URL the deployment fills in, resolved to the
563
+ * path it reaches.
564
+ *
565
+ * The source cannot settle this on its own, which is why the
566
+ * adapter leaves the hole in. `API_BASE` could be
567
+ * `http://backend.internal`, and then the path is `/orders`. It
568
+ * could equally be `/api/v2`, and then the path is
569
+ * `/api/v2/orders`. Putting the deployed value in and reading the
570
+ * path back out gets the right one either way.
571
+ *
572
+ * Only a hole at the front is a base URL. A hole further along is a
573
+ * route parameter, and `/orders/{id}` means every id rather than a
574
+ * variable somebody set.
575
+ */
576
+ groundName(semantics, deployedAs) {
577
+ if (semantics.path === null) {
578
+ return null;
579
+ }
580
+ const opening = OPENING_HOLE.exec(semantics.path);
581
+ if (opening === null) {
582
+ return null;
583
+ }
584
+ const [, label, rest] = opening;
585
+ const base = deployedAs(variableOf(label));
586
+ if (base === null) {
587
+ return null;
588
+ }
589
+ const grounded = pathAfterOrigin(`${base}${rest}`);
590
+ return { ...semantics, path: grounded === "" ? "/" : grounded };
591
+ },
539
592
  ruleBoundary: {
540
593
  // "METHOD /path": one leading token, then an absolute path.
541
594
  claims(raw) {
@@ -827,6 +880,13 @@ function semconvAttributes(binding) {
827
880
  function boundaryKey(binding) {
828
881
  return behaviorOf(binding.semantics).identityKey(binding.semantics);
829
882
  }
883
+ function groundedPairingKey(binding, deployedAs) {
884
+ const behavior = behaviorOf(binding.semantics);
885
+ const grounded = behavior.groundName?.(binding.semantics, deployedAs) ?? null;
886
+ return pairingKey(
887
+ grounded === null ? binding : { ...binding, semantics: grounded }
888
+ );
889
+ }
830
890
  function pairingKey(binding) {
831
891
  const behavior = behaviorOf(binding.semantics);
832
892
  return (behavior.pairingKey ?? behavior.identityKey)(binding.semantics);
@@ -1442,6 +1502,7 @@ function metricBinding(opts) {
1442
1502
  gqlIdentityKey,
1443
1503
  graphqlOperationBinding,
1444
1504
  graphqlResolverBinding,
1505
+ groundedPairingKey,
1445
1506
  hasNameHole,
1446
1507
  isGraphqlOperationBinding,
1447
1508
  messageBusBinding,
@@ -1458,6 +1519,7 @@ function metricBinding(opts) {
1458
1519
  pairingKey,
1459
1520
  parseBoundaryName,
1460
1521
  parseChannel,
1522
+ pathAfterOrigin,
1461
1523
  pathShape,
1462
1524
  patternHole,
1463
1525
  referenceFromName,
@@ -1471,6 +1533,7 @@ function metricBinding(opts) {
1471
1533
  semanticsAgree,
1472
1534
  semconvAttributes,
1473
1535
  servesRequest,
1536
+ statesAnOrigin,
1474
1537
  storageBinding,
1475
1538
  typeDefinitionKey,
1476
1539
  validateRule,
package/dist/index.d.cts CHANGED
@@ -117,6 +117,24 @@ interface BoundaryBehavior<S extends {
117
117
  * does not apply, and unknown means this declaration cannot settle it.
118
118
  */
119
119
  servesRequest?(semantics: S, method: string, path: string): MatchResult;
120
+ /**
121
+ * The same boundary with a name the deployment fills in put in.
122
+ *
123
+ * A call written as `fetch(`${process.env.API_BASE}/orders`)` gets
124
+ * part of its boundary from the source and part from whatever runs
125
+ * the code. Both parts have to be in before anybody can see that the
126
+ * two sides describe one boundary.
127
+ *
128
+ * `deployedAs` is already scoped to the unit this boundary belongs to.
129
+ * A protocol asks for a variable and gets back what this deployment
130
+ * sets it to, or null. Return null to leave the boundary as it is,
131
+ * which is right whenever nothing needs filling in or nothing can
132
+ * fill it.
133
+ *
134
+ * A protocol whose names are settled in the source leaves this
135
+ * undefined.
136
+ */
137
+ groundName?(semantics: S, deployedAs: (variable: string) => string | null): S | null;
120
138
  /**
121
139
  * Whether a provider produces a status and a body that a consumer
122
140
  * reads back. Every protocol has to state this, so that one added
@@ -219,6 +237,16 @@ declare function semconvAttributes(binding: BoundaryBinding): Record<string, str
219
237
  */
220
238
 
221
239
  declare function boundaryKey(binding: BoundaryBinding): string | null;
240
+ /**
241
+ * The bucket a boundary pairs in, with any name the deployment fills in
242
+ * put in first.
243
+ *
244
+ * A consumer that gets its base URL from the runtime states a different
245
+ * string from the provider it reaches, and the two are one boundary.
246
+ * Every protocol that has such a name says how to fill it in, and one
247
+ * whose names are settled in the source keys exactly as before.
248
+ */
249
+ declare function groundedPairingKey(binding: BoundaryBinding, deployedAs: (variable: string) => string | null): string | null;
222
250
  declare function pairingKey(binding: BoundaryBinding): string | null;
223
251
  declare function semanticsAgree(a: Semantics, b: Semantics): boolean;
224
252
  declare function boundaryLabel(binding: BoundaryBinding): string | null;
@@ -554,6 +582,30 @@ declare function applySuppressionsToFindings<T extends SuppressibleFinding>(find
554
582
  */
555
583
  declare function countsForThreshold(finding: SuppressibleFinding): boolean;
556
584
 
585
+ /**
586
+ * urlPath.ts: the path part of a URL, however the URL was written.
587
+ *
588
+ * Two sides of one boundary spell it differently. A provider says
589
+ * `/orders`, because a route is mounted at a path. A consumer says
590
+ * `http://backend.internal/orders`, because a call has to say which
591
+ * host. They are the same boundary, and pairing them means reading the
592
+ * path out of both.
593
+ *
594
+ * The query and the fragment come off with the origin. Neither picks a
595
+ * route: `/orders?page=2` reaches the same handler as `/orders`.
596
+ */
597
+ /**
598
+ * The path a URL states, with any origin, query, and fragment removed.
599
+ *
600
+ * Text that states no origin comes back with only the query and the
601
+ * fragment removed, since a relative URL is already a path. A URL that
602
+ * is nothing but an origin comes back as an empty string, which is what
603
+ * a caller wanting a path should treat as "the root".
604
+ */
605
+ declare function pathAfterOrigin(text: string): string;
606
+ /** Whether the text opens with an origin rather than with a path. */
607
+ declare function statesAnOrigin(text: string): boolean;
608
+
557
609
  /**
558
610
  * @suss/ir-core: primitives shared across suss IRs.
559
611
  *
@@ -684,4 +736,4 @@ declare function metricBinding(opts: {
684
736
  metricType: string | null;
685
737
  }): BoundaryBinding;
686
738
 
687
- export { type BoundaryBehavior, type BoundaryBinding, BoundaryBindingSchema, type BoundaryName, type BoundarySemanticsDefinition, type BusIdentityKey, type Channel, type ConfidenceInfo, ConfidenceInfoSchema, type ConfidenceLevel, ConfidenceLevelSchema, type ConfidenceSource, ConfidenceSourceSchema, type Corroboration, CorroborationSchema, DeployableUnit, type DispatchTable, type FindingSuppression, type FnIdentityKey, type GqlIdentityKey, type MatchResult, MessageBusSemantics, MessageBusTechnology, type MetricIdentityKey, type MetricSemantics, MetricSemanticsSchema, type NamePart, type ParsedChannel, type Reference, Semantics, type SemconvAttribute, type SemconvMapping, type SourceLocation, SourceLocationSchema, type SuppressableSeverity, type SuppressibleFinding, type SuppressionFile, SuppressionFileSchema, type SuppressionRule, SuppressionRuleSchema, TypeShape, applySuppressionsToFindings, bodyShapesMatch, boundaryKey, boundaryLabel, boundaryNameString, busIdentityKey, busesAgree, canPair, channelsPair, codeScopePath, countsForThreshold, dispatchByType, displayLabel, exchangesHttpResponses, fileInCodeScope, fixedTextLength, fnIdentityKey, formatChannel, functionCallBinding, gqlIdentityKey, graphqlOperationBinding, graphqlResolverBinding, hasNameHole, isGraphqlOperationBinding, messageBusBinding, metricBinding, metricIdentityKey, namePatternFromSub, namePatternKey, namesAgree, namesNothing, normalizeRuleBoundary, packageExportBinding, pairingKey, parseBoundaryName, parseChannel, patternHole, referenceFromName, referenceName, referenceOf, reportsUnpairedItself, restBinding, ruleBoundaryMatchesKey, runtimeConfigBinding, semanticsAgree, semconvAttributes, servesRequest, storageBinding, validateRule };
739
+ export { type BoundaryBehavior, type BoundaryBinding, BoundaryBindingSchema, type BoundaryName, type BoundarySemanticsDefinition, type BusIdentityKey, type Channel, type ConfidenceInfo, ConfidenceInfoSchema, type ConfidenceLevel, ConfidenceLevelSchema, type ConfidenceSource, ConfidenceSourceSchema, type Corroboration, CorroborationSchema, DeployableUnit, type DispatchTable, type FindingSuppression, type FnIdentityKey, type GqlIdentityKey, type MatchResult, MessageBusSemantics, MessageBusTechnology, type MetricIdentityKey, type MetricSemantics, MetricSemanticsSchema, type NamePart, type ParsedChannel, type Reference, Semantics, type SemconvAttribute, type SemconvMapping, type SourceLocation, SourceLocationSchema, type SuppressableSeverity, type SuppressibleFinding, type SuppressionFile, SuppressionFileSchema, type SuppressionRule, SuppressionRuleSchema, TypeShape, applySuppressionsToFindings, bodyShapesMatch, boundaryKey, boundaryLabel, boundaryNameString, busIdentityKey, busesAgree, canPair, channelsPair, codeScopePath, countsForThreshold, dispatchByType, displayLabel, exchangesHttpResponses, fileInCodeScope, fixedTextLength, fnIdentityKey, formatChannel, functionCallBinding, gqlIdentityKey, graphqlOperationBinding, graphqlResolverBinding, groundedPairingKey, hasNameHole, isGraphqlOperationBinding, messageBusBinding, metricBinding, metricIdentityKey, namePatternFromSub, namePatternKey, namesAgree, namesNothing, normalizeRuleBoundary, packageExportBinding, pairingKey, parseBoundaryName, parseChannel, pathAfterOrigin, patternHole, referenceFromName, referenceName, referenceOf, reportsUnpairedItself, restBinding, ruleBoundaryMatchesKey, runtimeConfigBinding, semanticsAgree, semconvAttributes, servesRequest, statesAnOrigin, storageBinding, validateRule };
package/dist/index.d.ts CHANGED
@@ -117,6 +117,24 @@ interface BoundaryBehavior<S extends {
117
117
  * does not apply, and unknown means this declaration cannot settle it.
118
118
  */
119
119
  servesRequest?(semantics: S, method: string, path: string): MatchResult;
120
+ /**
121
+ * The same boundary with a name the deployment fills in put in.
122
+ *
123
+ * A call written as `fetch(`${process.env.API_BASE}/orders`)` gets
124
+ * part of its boundary from the source and part from whatever runs
125
+ * the code. Both parts have to be in before anybody can see that the
126
+ * two sides describe one boundary.
127
+ *
128
+ * `deployedAs` is already scoped to the unit this boundary belongs to.
129
+ * A protocol asks for a variable and gets back what this deployment
130
+ * sets it to, or null. Return null to leave the boundary as it is,
131
+ * which is right whenever nothing needs filling in or nothing can
132
+ * fill it.
133
+ *
134
+ * A protocol whose names are settled in the source leaves this
135
+ * undefined.
136
+ */
137
+ groundName?(semantics: S, deployedAs: (variable: string) => string | null): S | null;
120
138
  /**
121
139
  * Whether a provider produces a status and a body that a consumer
122
140
  * reads back. Every protocol has to state this, so that one added
@@ -219,6 +237,16 @@ declare function semconvAttributes(binding: BoundaryBinding): Record<string, str
219
237
  */
220
238
 
221
239
  declare function boundaryKey(binding: BoundaryBinding): string | null;
240
+ /**
241
+ * The bucket a boundary pairs in, with any name the deployment fills in
242
+ * put in first.
243
+ *
244
+ * A consumer that gets its base URL from the runtime states a different
245
+ * string from the provider it reaches, and the two are one boundary.
246
+ * Every protocol that has such a name says how to fill it in, and one
247
+ * whose names are settled in the source keys exactly as before.
248
+ */
249
+ declare function groundedPairingKey(binding: BoundaryBinding, deployedAs: (variable: string) => string | null): string | null;
222
250
  declare function pairingKey(binding: BoundaryBinding): string | null;
223
251
  declare function semanticsAgree(a: Semantics, b: Semantics): boolean;
224
252
  declare function boundaryLabel(binding: BoundaryBinding): string | null;
@@ -554,6 +582,30 @@ declare function applySuppressionsToFindings<T extends SuppressibleFinding>(find
554
582
  */
555
583
  declare function countsForThreshold(finding: SuppressibleFinding): boolean;
556
584
 
585
+ /**
586
+ * urlPath.ts: the path part of a URL, however the URL was written.
587
+ *
588
+ * Two sides of one boundary spell it differently. A provider says
589
+ * `/orders`, because a route is mounted at a path. A consumer says
590
+ * `http://backend.internal/orders`, because a call has to say which
591
+ * host. They are the same boundary, and pairing them means reading the
592
+ * path out of both.
593
+ *
594
+ * The query and the fragment come off with the origin. Neither picks a
595
+ * route: `/orders?page=2` reaches the same handler as `/orders`.
596
+ */
597
+ /**
598
+ * The path a URL states, with any origin, query, and fragment removed.
599
+ *
600
+ * Text that states no origin comes back with only the query and the
601
+ * fragment removed, since a relative URL is already a path. A URL that
602
+ * is nothing but an origin comes back as an empty string, which is what
603
+ * a caller wanting a path should treat as "the root".
604
+ */
605
+ declare function pathAfterOrigin(text: string): string;
606
+ /** Whether the text opens with an origin rather than with a path. */
607
+ declare function statesAnOrigin(text: string): boolean;
608
+
557
609
  /**
558
610
  * @suss/ir-core: primitives shared across suss IRs.
559
611
  *
@@ -684,4 +736,4 @@ declare function metricBinding(opts: {
684
736
  metricType: string | null;
685
737
  }): BoundaryBinding;
686
738
 
687
- export { type BoundaryBehavior, type BoundaryBinding, BoundaryBindingSchema, type BoundaryName, type BoundarySemanticsDefinition, type BusIdentityKey, type Channel, type ConfidenceInfo, ConfidenceInfoSchema, type ConfidenceLevel, ConfidenceLevelSchema, type ConfidenceSource, ConfidenceSourceSchema, type Corroboration, CorroborationSchema, DeployableUnit, type DispatchTable, type FindingSuppression, type FnIdentityKey, type GqlIdentityKey, type MatchResult, MessageBusSemantics, MessageBusTechnology, type MetricIdentityKey, type MetricSemantics, MetricSemanticsSchema, type NamePart, type ParsedChannel, type Reference, Semantics, type SemconvAttribute, type SemconvMapping, type SourceLocation, SourceLocationSchema, type SuppressableSeverity, type SuppressibleFinding, type SuppressionFile, SuppressionFileSchema, type SuppressionRule, SuppressionRuleSchema, TypeShape, applySuppressionsToFindings, bodyShapesMatch, boundaryKey, boundaryLabel, boundaryNameString, busIdentityKey, busesAgree, canPair, channelsPair, codeScopePath, countsForThreshold, dispatchByType, displayLabel, exchangesHttpResponses, fileInCodeScope, fixedTextLength, fnIdentityKey, formatChannel, functionCallBinding, gqlIdentityKey, graphqlOperationBinding, graphqlResolverBinding, hasNameHole, isGraphqlOperationBinding, messageBusBinding, metricBinding, metricIdentityKey, namePatternFromSub, namePatternKey, namesAgree, namesNothing, normalizeRuleBoundary, packageExportBinding, pairingKey, parseBoundaryName, parseChannel, patternHole, referenceFromName, referenceName, referenceOf, reportsUnpairedItself, restBinding, ruleBoundaryMatchesKey, runtimeConfigBinding, semanticsAgree, semconvAttributes, servesRequest, storageBinding, validateRule };
739
+ export { type BoundaryBehavior, type BoundaryBinding, BoundaryBindingSchema, type BoundaryName, type BoundarySemanticsDefinition, type BusIdentityKey, type Channel, type ConfidenceInfo, ConfidenceInfoSchema, type ConfidenceLevel, ConfidenceLevelSchema, type ConfidenceSource, ConfidenceSourceSchema, type Corroboration, CorroborationSchema, DeployableUnit, type DispatchTable, type FindingSuppression, type FnIdentityKey, type GqlIdentityKey, type MatchResult, MessageBusSemantics, MessageBusTechnology, type MetricIdentityKey, type MetricSemantics, MetricSemanticsSchema, type NamePart, type ParsedChannel, type Reference, Semantics, type SemconvAttribute, type SemconvMapping, type SourceLocation, SourceLocationSchema, type SuppressableSeverity, type SuppressibleFinding, type SuppressionFile, SuppressionFileSchema, type SuppressionRule, SuppressionRuleSchema, TypeShape, applySuppressionsToFindings, bodyShapesMatch, boundaryKey, boundaryLabel, boundaryNameString, busIdentityKey, busesAgree, canPair, channelsPair, codeScopePath, countsForThreshold, dispatchByType, displayLabel, exchangesHttpResponses, fileInCodeScope, fixedTextLength, fnIdentityKey, formatChannel, functionCallBinding, gqlIdentityKey, graphqlOperationBinding, graphqlResolverBinding, groundedPairingKey, hasNameHole, isGraphqlOperationBinding, messageBusBinding, metricBinding, metricIdentityKey, namePatternFromSub, namePatternKey, namesAgree, namesNothing, normalizeRuleBoundary, packageExportBinding, pairingKey, parseBoundaryName, parseChannel, pathAfterOrigin, patternHole, referenceFromName, referenceName, referenceOf, reportsUnpairedItself, restBinding, ruleBoundaryMatchesKey, runtimeConfigBinding, semanticsAgree, semconvAttributes, servesRequest, statesAnOrigin, storageBinding, validateRule };
package/dist/index.js CHANGED
@@ -29,12 +29,14 @@ import {
29
29
  metricIdentityKey,
30
30
  normalizePath,
31
31
  parseChannel,
32
+ pathAfterOrigin,
32
33
  pathShape,
33
34
  routePathAdmits,
34
35
  semconvMappingOf,
36
+ statesAnOrigin,
35
37
  typeDefinitionKey,
36
38
  withDefinitionsInlined
37
- } from "./chunk-AQYS5AC6.js";
39
+ } from "./chunk-HSJYPBJU.js";
38
40
 
39
41
  // src/semconv.ts
40
42
  function semconvAttributes(binding) {
@@ -59,6 +61,13 @@ function semconvAttributes(binding) {
59
61
  function boundaryKey(binding) {
60
62
  return behaviorOf(binding.semantics).identityKey(binding.semantics);
61
63
  }
64
+ function groundedPairingKey(binding, deployedAs) {
65
+ const behavior = behaviorOf(binding.semantics);
66
+ const grounded = behavior.groundName?.(binding.semantics, deployedAs) ?? null;
67
+ return pairingKey(
68
+ grounded === null ? binding : { ...binding, semantics: grounded }
69
+ );
70
+ }
62
71
  function pairingKey(binding) {
63
72
  const behavior = behaviorOf(binding.semantics);
64
73
  return (behavior.pairingKey ?? behavior.identityKey)(binding.semantics);
@@ -673,6 +682,7 @@ export {
673
682
  gqlIdentityKey,
674
683
  graphqlOperationBinding,
675
684
  graphqlResolverBinding,
685
+ groundedPairingKey,
676
686
  hasNameHole,
677
687
  isGraphqlOperationBinding,
678
688
  messageBusBinding,
@@ -689,6 +699,7 @@ export {
689
699
  pairingKey,
690
700
  parseBoundaryName,
691
701
  parseChannel,
702
+ pathAfterOrigin,
692
703
  pathShape,
693
704
  patternHole,
694
705
  referenceFromName,
@@ -702,6 +713,7 @@ export {
702
713
  semanticsAgree,
703
714
  semconvAttributes,
704
715
  servesRequest,
716
+ statesAnOrigin,
705
717
  storageBinding,
706
718
  typeDefinitionKey,
707
719
  validateRule,
package/dist/schemas.cjs CHANGED
@@ -344,6 +344,17 @@ var metricSemantics = defineBoundarySemantics({
344
344
 
345
345
  // src/semantics/rest.ts
346
346
  var import_zod6 = require("zod");
347
+
348
+ // src/urlPath.ts
349
+ var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
350
+ var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
351
+ function pathAfterOrigin(text) {
352
+ const withoutOrigin = text.replace(SCHEME_ORIGIN, "").replace(PROTOCOL_RELATIVE_ORIGIN, "");
353
+ const cut = withoutOrigin.search(/[?#]/);
354
+ return cut === -1 ? withoutOrigin : withoutOrigin.slice(0, cut);
355
+ }
356
+
357
+ // src/semantics/rest.ts
347
358
  var RestSemanticsSchema = import_zod6.z.object({
348
359
  name: import_zod6.z.literal("rest"),
349
360
  /**
@@ -362,6 +373,11 @@ var RestSemanticsSchema = import_zod6.z.object({
362
373
  */
363
374
  declaredResponses: import_zod6.z.array(import_zod6.z.number()).optional()
364
375
  });
376
+ var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
377
+ function variableOf(label) {
378
+ const parts = label.split(".");
379
+ return parts[parts.length - 1] ?? label;
380
+ }
365
381
  function normalizePath(path) {
366
382
  let normalized = path.replace(/:([a-zA-Z_]\w*)/g, "{$1}");
367
383
  if (normalized.length > 1 && normalized.endsWith("/")) {
@@ -471,6 +487,37 @@ var restSemantics = defineBoundarySemantics({
471
487
  }
472
488
  return routePathAdmits(semantics.path, path) ? "match" : "nomatch";
473
489
  },
490
+ /**
491
+ * A call whose base URL the deployment fills in, resolved to the
492
+ * path it reaches.
493
+ *
494
+ * The source cannot settle this on its own, which is why the
495
+ * adapter leaves the hole in. `API_BASE` could be
496
+ * `http://backend.internal`, and then the path is `/orders`. It
497
+ * could equally be `/api/v2`, and then the path is
498
+ * `/api/v2/orders`. Putting the deployed value in and reading the
499
+ * path back out gets the right one either way.
500
+ *
501
+ * Only a hole at the front is a base URL. A hole further along is a
502
+ * route parameter, and `/orders/{id}` means every id rather than a
503
+ * variable somebody set.
504
+ */
505
+ groundName(semantics, deployedAs) {
506
+ if (semantics.path === null) {
507
+ return null;
508
+ }
509
+ const opening = OPENING_HOLE.exec(semantics.path);
510
+ if (opening === null) {
511
+ return null;
512
+ }
513
+ const [, label, rest] = opening;
514
+ const base = deployedAs(variableOf(label));
515
+ if (base === null) {
516
+ return null;
517
+ }
518
+ const grounded = pathAfterOrigin(`${base}${rest}`);
519
+ return { ...semantics, path: grounded === "" ? "/" : grounded };
520
+ },
474
521
  ruleBoundary: {
475
522
  // "METHOD /path": one leading token, then an absolute path.
476
523
  claims(raw) {
package/dist/schemas.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  TypeShapeSchema,
18
18
  typeDefinitionKey,
19
19
  withDefinitionsInlined
20
- } from "./chunk-AQYS5AC6.js";
20
+ } from "./chunk-HSJYPBJU.js";
21
21
  export {
22
22
  BoundaryBindingSchema,
23
23
  ConfidenceInfoSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/ir-core",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "Shared IR primitives for suss — type shapes, boundary bindings, source locations, and confidence. Imported by both @suss/behavioral-ir and @suss/intent-ir so neither depends on the other.",
6
6
  "keywords": [