@suss/ir-core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,17 +31,39 @@ The boundary key uses only the subject, so both forms end up in the same bucket
31
31
 
32
32
  Schemas are the single source of truth (`@suss/ir-core/schemas`); the types are derived from them. The recursive `TypeShape` is a hand-written named export so consuming packages reference it by name across the package boundary rather than inlining the recursion.
33
33
 
34
- ## Names with a hole in them
34
+ ## Boundary names
35
35
 
36
- A deployed resource is often called something built at deploy time. A template writes `!Sub "${StageName}-orders-v1"` and the code writes `` `${stage}-orders-v1` ``, so neither side states a string, both agree about the fixed text, and each spells the parameter its own way. A name is written here as fixed text with `{}` holes, and `namesAgree` says whether two of them are the same name.
36
+ A container or an access path in a summary is a string that means one of three things, and the braces alone say which:
37
37
 
38
- A hole stops at the separator between it and what comes next. No separator works for every project, so the rule does not name one, it takes the separator from the pattern: when the fixed text after a hole starts with a character that is not a letter or a digit, that character divides the deploy-time value from the rest of the name, and the value may not contain it. So `{env}-publications-v1` covers `prod-publications-v1`, and it does not cover `prod-creator-publications-v1`, which the hole could only reach by swallowing a `-`.
38
+ - A **literal** (`orders-v1`) is the name itself.
39
+ - A **pattern** (`{stage}-orders-v1`) is fixed text with `{}` holes for the parts a deployment fills in. A template writes `!Sub "${StageName}-orders-v1"` and the code writes `` `${stage}-orders-v1` ``: neither states a string, both agree about the fixed text, and each spells the parameter its own way.
40
+ - A **reference** (`{location.bucket}`, `{ORDER_TABLE}`) is one hole and no fixed text, saying where to go and ask rather than what the name is.
39
41
 
40
- A greedy hole was the earlier rule, and a module declaring both of those tables had one storage access pair with each of them. The table the code never touches keys on something else, so the run reported a selector mismatch on a boundary that code never reaches.
42
+ `boundaryName.ts` owns the syntax in both directions. `parseBoundaryName` turns a string into the discriminated `BoundaryName` value, `boundaryNameString` turns one back into exactly the string it came from, and everything else, `namesAgree`, `namesNothing`, `fixedTextLength`, `referenceName`, `referenceFromName`, is a view over that pair. Nothing outside the module reads or writes the braces itself: a second parser can disagree about which of the three a string is, and a second printer can spell a value the parser cannot read back. Both have happened. The two halves of caller grounding once spelled a reference differently and never met (#456), which is why writing one and reading one are the same module rather than a format each side implements for itself. `check:name-syntax` in CI keeps it that way.
41
43
 
42
- A hole at the end of a name, and a hole whose next character is a letter or a digit, has no separator to stop at, and it still covers anything. That costs a pair when the value has the separator inside it: `{region}-orders` does not cover `us-east-1-orders`, though `orders-{region}` still covers `orders-us-east-1`. Missing a pair is the better failure of the two, since a wrong match reports findings about a store the code never touches.
44
+ The wire format is the string. Summaries on disk carry `orders-v1`, `{stage}-orders-v1`, `{location.table}` unchanged, so nothing about this layout is a schema change, and an old summary reads the same as a new one.
43
45
 
44
- One name can still be covered by two patterns. A hole at the end covers anything, so `orders-{suffix}` and `orders-blue-{suffix}` both cover `orders-blue-v1`. Choosing between them belongs to whichever pass picks a provider for an access, and `fixedTextLength` is what such a pass ranks by: the pattern that states more fixed text is the more specific one, and two patterns that state the same amount settle nothing between them.
46
+ ### How two names pair
47
+
48
+ `namesAgree` says whether two names are the same name. Two patterns agree when their fixed parts line up, since a hole on one side meets a hole on the other. A pattern and a literal agree when the fixed text is where the pattern says it is, which is what happens when one side hardcoded what the other parameterized. A reference agrees with nothing until something settles it, since one bare hole would otherwise agree with every name there is.
49
+
50
+ A hole covers anything, because a name has no separator every project agrees on: a region is written `us-east-1`, and a hole that stopped at the first hyphen would miss it. The cost is that one name can be covered by two patterns, `orders-{suffix}` and `orders-blue-{suffix}` both cover `orders-blue-v1`, so choosing between them belongs to whichever pass picks a provider for an access. `fixedTextLength` is what such a pass ranks by: the pattern that states more fixed text is the more specific one, and two patterns that state the same amount settle nothing between them.
51
+
52
+ ### A name that says where to go and ask
53
+
54
+ Some code cannot say what it reaches. A storage layer takes the bucket as an argument, and a service reads its table name out of the deployment. Neither states a name, and a name is what the rest of suss pairs on, so both write a reference instead.
55
+
56
+ ```ts
57
+ referenceName({ root: "location", fields: ["bucket"] }); // "{location.bucket}"
58
+ referenceName({ root: "ORDER_TABLE", fields: [] }); // "{ORDER_TABLE}"
59
+ referenceFromName("{location.bucket}"); // { root: "location", fields: ["bucket"] }
60
+ ```
61
+
62
+ The root is the value the code starts from, and the fields are what it reads inside that value. A language adapter writes one while reading source, and the checker settles it much later by joining over a whole run: `{location.bucket}` against what each caller passed for `location`, `{ORDER_TABLE}` against what the deployment sets. Whether the root is a parameter of the unit or a variable the deployment sets is not in the string, on purpose: it depends on the unit's inputs and their roles, which the grounding pass already has in hand, and a bare `{X}` is deliberately tried both ways there. A root kind written into the name would either be ignored or change which of those runs.
63
+
64
+ ### Two look-alike conventions that stay apart
65
+
66
+ A REST route path also spells its parameters with braces (`/users/{id}`), but a route hole stops at the `/` between segments, so route paths keep their own comparison beside the REST semantics. A message-bus channel never uses braces at all; its `bus#subject` form has its own module, described above.
45
67
 
46
68
  ## Words OpenTelemetry already has
47
69
 
@@ -338,6 +338,9 @@ function normalizePath(path) {
338
338
  );
339
339
  return normalized;
340
340
  }
341
+ function pathShape(path) {
342
+ return normalizePath(path).replace(/\{[^}]*\}/g, "{}");
343
+ }
341
344
  function methodsAgree(a, b) {
342
345
  if (a === null || b === null) {
343
346
  return false;
@@ -389,12 +392,17 @@ var restSemantics = defineBoundarySemantics({
389
392
  * settles it inside the bucket. The identity key stays what a reader
390
393
  * sees and a suppression targets ("GET /users", "* /users"), and
391
394
  * this key groups what a reader still tells apart.
395
+ *
396
+ * A parameter's name goes too. A spec written by hand says
397
+ * `{userId}` where the Express route that serves it says `:id`, and
398
+ * both match the same requests, so pairing on the name gave two
399
+ * boundaries that never met and a run with nothing to report.
392
400
  */
393
401
  pairingKey(semantics) {
394
402
  if (semantics.method === null || semantics.path === null) {
395
403
  return null;
396
404
  }
397
- return `rest ${normalizePath(semantics.path)}`;
405
+ return `rest ${pathShape(semantics.path)}`;
398
406
  },
399
407
  sidesAgree(a, b) {
400
408
  return methodsAgree(a.method, b.method);
@@ -503,7 +511,9 @@ var StorageSemanticsSchema = z9.object({
503
511
  * The table, bucket, collection, or index, as the source declares
504
512
  * it, or null when the source states one this reader could not
505
513
  * settle. A null container pairs with nothing, rather than with
506
- * whatever its source text happens to spell.
514
+ * whatever its source text happens to spell. The string is in the
515
+ * boundary-name syntax: a literal, a pattern with `{}` holes, or a
516
+ * reference, and `parseBoundaryName` is the one reader of it.
507
517
  */
508
518
  container: z9.string().nullable(),
509
519
  /**
@@ -710,6 +720,7 @@ export {
710
720
  MetricSemanticsSchema,
711
721
  RestSemanticsSchema,
712
722
  normalizePath,
723
+ pathShape,
713
724
  methodsAgree,
714
725
  routePathAdmits,
715
726
  DeployableUnitSchema,
package/dist/index.cjs CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  bodyShapesMatch: () => bodyShapesMatch,
44
44
  boundaryKey: () => boundaryKey,
45
45
  boundaryLabel: () => boundaryLabel,
46
+ boundaryNameString: () => boundaryNameString,
46
47
  busIdentityKey: () => busIdentityKey,
47
48
  busesAgree: () => busesAgree,
48
49
  canPair: () => canPair,
@@ -75,7 +76,13 @@ __export(index_exports, {
75
76
  normalizeRuleBoundary: () => normalizeRuleBoundary,
76
77
  packageExportBinding: () => packageExportBinding,
77
78
  pairingKey: () => pairingKey,
79
+ parseBoundaryName: () => parseBoundaryName,
78
80
  parseChannel: () => parseChannel,
81
+ pathShape: () => pathShape,
82
+ patternHole: () => patternHole,
83
+ referenceFromName: () => referenceFromName,
84
+ referenceName: () => referenceName,
85
+ referenceOf: () => referenceOf,
79
86
  reportsUnpairedItself: () => reportsUnpairedItself,
80
87
  restBinding: () => restBinding,
81
88
  routePathAdmits: () => routePathAdmits,
@@ -431,6 +438,9 @@ function normalizePath(path) {
431
438
  );
432
439
  return normalized;
433
440
  }
441
+ function pathShape(path) {
442
+ return normalizePath(path).replace(/\{[^}]*\}/g, "{}");
443
+ }
434
444
  function methodsAgree(a, b) {
435
445
  if (a === null || b === null) {
436
446
  return false;
@@ -482,12 +492,17 @@ var restSemantics = defineBoundarySemantics({
482
492
  * settles it inside the bucket. The identity key stays what a reader
483
493
  * sees and a suppression targets ("GET /users", "* /users"), and
484
494
  * this key groups what a reader still tells apart.
495
+ *
496
+ * A parameter's name goes too. A spec written by hand says
497
+ * `{userId}` where the Express route that serves it says `:id`, and
498
+ * both match the same requests, so pairing on the name gave two
499
+ * boundaries that never met and a run with nothing to report.
485
500
  */
486
501
  pairingKey(semantics) {
487
502
  if (semantics.method === null || semantics.path === null) {
488
503
  return null;
489
504
  }
490
- return `rest ${normalizePath(semantics.path)}`;
505
+ return `rest ${pathShape(semantics.path)}`;
491
506
  },
492
507
  sidesAgree(a, b) {
493
508
  return methodsAgree(a.method, b.method);
@@ -596,7 +611,9 @@ var StorageSemanticsSchema = import_zod9.z.object({
596
611
  * The table, bucket, collection, or index, as the source declares
597
612
  * it, or null when the source states one this reader could not
598
613
  * settle. A null container pairs with nothing, rather than with
599
- * whatever its source text happens to spell.
614
+ * whatever its source text happens to spell. The string is in the
615
+ * boundary-name syntax: a literal, a pattern with `{}` holes, or a
616
+ * reference, and `parseBoundaryName` is the one reader of it.
600
617
  */
601
618
  container: import_zod9.z.string().nullable(),
602
619
  /**
@@ -858,29 +875,47 @@ function servesRequest(binding, method, path) {
858
875
  return serves(binding.semantics, method, path);
859
876
  }
860
877
 
861
- // src/codeScope.ts
862
- function codeScopePath(raw) {
863
- return raw.trim().replace(/^\.\//, "").replace(/\/+$/, "");
864
- }
865
- function fileInCodeScope(file, scope) {
866
- const prefix = codeScopePath(scope);
867
- if (prefix === "" || prefix === ".") {
868
- return true;
869
- }
870
- const path = codeScopePath(file);
871
- return path === prefix || path.startsWith(`${prefix}/`);
872
- }
873
-
874
878
  // src/dispatch.ts
875
879
  function dispatchByType(table, value) {
876
880
  const handler = table[value.type];
877
881
  return handler(value);
878
882
  }
879
883
 
880
- // src/namePattern.ts
881
- var HOLE = /\{[^}]*\}/g;
884
+ // src/boundaryName.ts
885
+ var HOLE_SPLIT = /(\{[^}]*\})/;
882
886
  var SUB_TOKEN = /\$\{([^}]*)\}/g;
883
887
  var SUB_ESCAPE = /\$\{!/;
888
+ function isHole(piece) {
889
+ return piece.startsWith("{") && piece.endsWith("}");
890
+ }
891
+ function parseBoundaryName(name) {
892
+ const parts = name.split(HOLE_SPLIT).filter((piece) => piece !== "").map(
893
+ (piece) => isHole(piece) ? { type: "hole", label: piece.slice(1, -1) } : { type: "text", text: piece }
894
+ );
895
+ const only = parts.length === 1 ? parts[0] : void 0;
896
+ if (only !== void 0 && only.type === "hole") {
897
+ return { type: "reference", path: only.label.split(".") };
898
+ }
899
+ if (parts.some((part) => part.type === "hole")) {
900
+ return { type: "pattern", parts };
901
+ }
902
+ return { type: "literal", value: name };
903
+ }
904
+ var partString = {
905
+ text: (part) => part.text,
906
+ hole: (part) => patternHole(part.label)
907
+ };
908
+ var nameString = {
909
+ literal: (name) => name.value,
910
+ pattern: (name) => name.parts.map((part) => dispatchByType(partString, part)).join(""),
911
+ reference: (name) => patternHole(name.path.join("."))
912
+ };
913
+ function boundaryNameString(name) {
914
+ return dispatchByType(nameString, name);
915
+ }
916
+ function patternHole(label) {
917
+ return `{${label}}`;
918
+ }
884
919
  function namePatternFromSub(value) {
885
920
  if (Array.isArray(value)) {
886
921
  return typeof value[0] === "string" ? namePatternFromSub(value[0]) : null;
@@ -891,56 +926,101 @@ function namePatternFromSub(value) {
891
926
  if (SUB_ESCAPE.test(value)) {
892
927
  return null;
893
928
  }
894
- return value.replace(SUB_TOKEN, (_whole, inner) => `{${inner}}`);
929
+ return value.replace(
930
+ SUB_TOKEN,
931
+ (_whole, inner) => patternHole(inner)
932
+ );
895
933
  }
896
934
  function namesNothing(name) {
897
- return /^\{[^}]*\}$/.test(name);
935
+ return parseBoundaryName(name).type === "reference";
898
936
  }
899
937
  function hasNameHole(name) {
900
- HOLE.lastIndex = 0;
901
- return HOLE.test(name);
938
+ return parseBoundaryName(name).type !== "literal";
902
939
  }
903
940
  function namePatternKey(name) {
904
- return name.replace(HOLE, "{}");
941
+ return dispatchByType(
942
+ {
943
+ literal: (parsed) => parsed.value,
944
+ pattern: (parsed) => boundaryNameString({
945
+ type: "pattern",
946
+ parts: parsed.parts.map(
947
+ (part) => part.type === "hole" ? { type: "hole", label: "" } : part
948
+ )
949
+ }),
950
+ reference: () => patternHole("")
951
+ },
952
+ parseBoundaryName(name)
953
+ );
905
954
  }
906
955
  function fixedTextLength(name) {
907
- return name.replace(HOLE, "").length;
908
- }
909
- function isHole(part) {
910
- return part.startsWith("{") && part.endsWith("}");
956
+ return dispatchByType(
957
+ {
958
+ literal: (parsed) => parsed.value.length,
959
+ pattern: (parsed) => parsed.parts.reduce(
960
+ (total, part) => total + (part.type === "text" ? part.text.length : 0),
961
+ 0
962
+ ),
963
+ reference: () => 0
964
+ },
965
+ parseBoundaryName(name)
966
+ );
911
967
  }
912
968
  function quote(text) {
913
969
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
914
970
  }
915
- function holeSource(_following) {
916
- return ".+";
917
- }
918
971
  function admits(pattern, name) {
919
- const parts = pattern.split(/(\{[^}]*\})/);
920
- const source = parts.map(
921
- (part, index) => isHole(part) ? holeSource(parts[index + 1] ?? "") : quote(part)
922
- ).join("");
972
+ const source = pattern.parts.map((part) => part.type === "hole" ? ".+" : quote(part.text)).join("");
923
973
  return new RegExp(`^${source}$`).test(name);
924
974
  }
925
975
  function namesAgree(a, b) {
926
- if (namesNothing(a) || namesNothing(b)) {
976
+ const left = parseBoundaryName(a);
977
+ const right = parseBoundaryName(b);
978
+ if (left.type === "reference" || right.type === "reference") {
927
979
  return false;
928
980
  }
929
- if (a === b) {
930
- return true;
931
- }
932
- const aHasHole = hasNameHole(a);
933
- const bHasHole = hasNameHole(b);
934
- if (aHasHole && bHasHole) {
981
+ if (left.type === "pattern" && right.type === "pattern") {
935
982
  return namePatternKey(a) === namePatternKey(b);
936
983
  }
937
- if (aHasHole) {
938
- return admits(a, b);
984
+ if (left.type === "pattern") {
985
+ return admits(left, b);
939
986
  }
940
- if (bHasHole) {
941
- return admits(b, a);
987
+ if (right.type === "pattern") {
988
+ return admits(right, a);
942
989
  }
943
- return false;
990
+ return a === b;
991
+ }
992
+ function referenceName(reference) {
993
+ const path = [reference.root, ...reference.fields];
994
+ if (path.some((part) => part === "")) {
995
+ return null;
996
+ }
997
+ return boundaryNameString({ type: "reference", path });
998
+ }
999
+ function referenceOf(name) {
1000
+ if (name.type !== "reference") {
1001
+ return null;
1002
+ }
1003
+ const root = name.path[0];
1004
+ if (root === void 0 || name.path.some((part) => part === "")) {
1005
+ return null;
1006
+ }
1007
+ return { root, fields: name.path.slice(1) };
1008
+ }
1009
+ function referenceFromName(name) {
1010
+ return referenceOf(parseBoundaryName(name));
1011
+ }
1012
+
1013
+ // src/codeScope.ts
1014
+ function codeScopePath(raw) {
1015
+ return raw.trim().replace(/^\.\//, "").replace(/\/+$/, "");
1016
+ }
1017
+ function fileInCodeScope(file, scope) {
1018
+ const prefix = codeScopePath(scope);
1019
+ if (prefix === "" || prefix === ".") {
1020
+ return true;
1021
+ }
1022
+ const path = codeScopePath(file);
1023
+ return path === prefix || path.startsWith(`${prefix}/`);
944
1024
  }
945
1025
 
946
1026
  // src/suppressions.ts
@@ -1343,6 +1423,7 @@ function metricBinding(opts) {
1343
1423
  bodyShapesMatch,
1344
1424
  boundaryKey,
1345
1425
  boundaryLabel,
1426
+ boundaryNameString,
1346
1427
  busIdentityKey,
1347
1428
  busesAgree,
1348
1429
  canPair,
@@ -1375,7 +1456,13 @@ function metricBinding(opts) {
1375
1456
  normalizeRuleBoundary,
1376
1457
  packageExportBinding,
1377
1458
  pairingKey,
1459
+ parseBoundaryName,
1378
1460
  parseChannel,
1461
+ pathShape,
1462
+ patternHole,
1463
+ referenceFromName,
1464
+ referenceName,
1465
+ referenceOf,
1379
1466
  reportsUnpairedItself,
1380
1467
  restBinding,
1381
1468
  routePathAdmits,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { T as TypeShape, M as MessageBusSemantics, S as Semantics, B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, d as SourceLocationSchema, e as MessageBusTechnology, D as DeployableUnit } from './schemas-CGR6qjFf.cjs';
3
- export { f as DeployableUnitSchema, F as FunctionCallSemantics, g as FunctionCallSemanticsSchema, G as GraphqlOperationSemantics, h as GraphqlOperationSemanticsSchema, i as GraphqlResolverSemantics, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, R as RestSemantics, l as RestSemanticsSchema, m as RuntimeConfigSemantics, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, p as StorageSemantics, q as StorageSemanticsSchema, r as TypeShapeSchema, s as ecsContainerInstanceName, t as methodsAgree, u as normalizePath, v as routePathAdmits, w as typeDefinitionKey, x as withDefinitionsInlined } from './schemas-CGR6qjFf.cjs';
2
+ import { T as TypeShape, M as MessageBusSemantics, S as Semantics, B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, d as SourceLocationSchema, e as MessageBusTechnology, D as DeployableUnit } from './schemas-k-169oiY.cjs';
3
+ export { f as DeployableUnitSchema, F as FunctionCallSemantics, g as FunctionCallSemanticsSchema, G as GraphqlOperationSemantics, h as GraphqlOperationSemanticsSchema, i as GraphqlResolverSemantics, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, R as RestSemantics, l as RestSemanticsSchema, m as RuntimeConfigSemantics, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, p as StorageSemantics, q as StorageSemanticsSchema, r as TypeShapeSchema, s as ecsContainerInstanceName, t as methodsAgree, u as normalizePath, v as pathShape, w as routePathAdmits, x as typeDefinitionKey, y as withDefinitionsInlined } from './schemas-k-169oiY.cjs';
4
4
 
5
5
  /**
6
6
  * Comparing two TypeShapes structurally.
@@ -243,6 +243,115 @@ declare function reportsUnpairedItself(binding: BoundaryBinding): boolean;
243
243
  */
244
244
  declare function servesRequest(binding: BoundaryBinding, method: string, path: string): MatchResult | null;
245
245
 
246
+ /**
247
+ * The boundary-name syntax, parsed and printed in one place.
248
+ *
249
+ * A name in a summary means one of three things, told apart by the
250
+ * braces alone: a literal (`orders-v1`), a pattern with deploy-time
251
+ * holes (`{stage}-orders-v1`), or a reference saying where to go and
252
+ * ask (`{location.bucket}`, `{ORDER_TABLE}`). A second parser can
253
+ * disagree about which is which, and a second printer can spell a
254
+ * value this one cannot read back, so everything that touches the
255
+ * braces calls this module: `parseBoundaryName` and
256
+ * `boundaryNameString` are the two directions, and the helpers below
257
+ * are views over them. The package README tells the longer story,
258
+ * including why REST route paths and message-bus channels stay apart.
259
+ */
260
+ /** One piece of a pattern: text the writer stated, or a hole. */
261
+ type NamePart = {
262
+ type: "text";
263
+ text: string;
264
+ } | {
265
+ type: "hole";
266
+ label: string;
267
+ };
268
+ /**
269
+ * What a name string means. A `reference` keeps its raw dot-separated
270
+ * path, and `referenceOf` is the view that checks the parts are all
271
+ * present, so a malformed spelling still classifies as a reference and
272
+ * still pairs with nothing.
273
+ */
274
+ type BoundaryName = {
275
+ type: "literal";
276
+ value: string;
277
+ } | {
278
+ type: "pattern";
279
+ parts: NamePart[];
280
+ } | {
281
+ type: "reference";
282
+ path: string[];
283
+ };
284
+ /** What a name string means. The exact inverse of `boundaryNameString`. */
285
+ declare function parseBoundaryName(name: string): BoundaryName;
286
+ /** The one serializer. Everything a summary spells goes through here. */
287
+ declare function boundaryNameString(name: BoundaryName): string;
288
+ /**
289
+ * How a hole is spelled. A reader that assembles a pattern a part at a
290
+ * time, the way the adapter's name reader does, mints each hole here
291
+ * so the spelling cannot drift from the parse above.
292
+ */
293
+ declare function patternHole(label: string): string;
294
+ /**
295
+ * The name a CloudFormation `Fn::Sub` value states. The array form
296
+ * takes its template from the first element, and the variable map only
297
+ * says where a substitution comes from, which does not change the name.
298
+ */
299
+ declare function namePatternFromSub(value: unknown): string | null;
300
+ /**
301
+ * Whether a name says only that somebody else knows it. A wrapper that
302
+ * takes its bucket as an argument states `{location.bucket}` and
303
+ * nothing more, which says which parameter to ask rather than which
304
+ * bucket. A name like that agrees with nothing until something grounds
305
+ * it, since otherwise it would agree with every name there is.
306
+ */
307
+ declare function namesNothing(name: string): boolean;
308
+ /** Whether a name has anything a source left for deploy time to fill. */
309
+ declare function hasNameHole(name: string): boolean;
310
+ /**
311
+ * The form two patterns are compared in: every hole reduced to the same
312
+ * token, since the two sides pick their own name for the parameter.
313
+ */
314
+ declare function namePatternKey(name: string): string;
315
+ /** How much of a name the writer stated rather than left for deploy time. */
316
+ declare function fixedTextLength(name: string): number;
317
+ /**
318
+ * Whether two names are the same name. Two patterns agree when their
319
+ * fixed parts line up, since a hole on one side meets a hole on the
320
+ * other. A pattern and a concrete name agree when the fixed parts are
321
+ * where the pattern says they are, which is what happens when one side
322
+ * hardcoded what the other parameterized. A reference agrees with
323
+ * nothing on either side.
324
+ */
325
+ declare function namesAgree(a: string, b: string): boolean;
326
+ /** Where a reference says to go and ask. */
327
+ interface Reference {
328
+ /**
329
+ * The value the code starts from: a parameter of the unit the
330
+ * reference was written in, or a variable the deployment sets. Which
331
+ * of those it is depends on the unit's inputs, so the reader that
332
+ * grounds a reference decides, not the string.
333
+ */
334
+ root: string;
335
+ /** The fields to read inside it, outermost first. */
336
+ fields: string[];
337
+ }
338
+ /**
339
+ * How a reference is written. Null when a part of it is empty, since a
340
+ * reference has to say what to ask about.
341
+ */
342
+ declare function referenceName(reference: Reference): string | null;
343
+ /**
344
+ * The place a parsed reference says to ask, or null for a name that is
345
+ * not a reference, or for a reference with a part missing, which says
346
+ * nothing anybody could answer.
347
+ */
348
+ declare function referenceOf(name: BoundaryName): Reference | null;
349
+ /**
350
+ * The reference a name states, or null when the name states a name
351
+ * rather than where to go and ask.
352
+ */
353
+ declare function referenceFromName(name: string): Reference | null;
354
+
246
355
  /**
247
356
  * Deciding when two message-bus channel strings mean the same thing.
248
357
  *
@@ -325,53 +434,6 @@ declare function dispatchByType<T extends {
325
434
  type: string;
326
435
  }, R>(table: DispatchTable<T, R>, value: T): R;
327
436
 
328
- /**
329
- * A name two sides state with a hole in it.
330
- *
331
- * A deployed resource is often called something built at deploy time:
332
- * a template writes `!Sub "${StageName}-orders-v1"` and the code writes
333
- * `` `${stage}-orders-v1` ``. Neither side states a string, both agree
334
- * about the fixed text, and each spells the parameter its own way. So a
335
- * name is written here as fixed text with `{}` holes, and two names
336
- * agree when their fixed parts line up.
337
- *
338
- * A hole covers a whole parameter rather than one piece of a name,
339
- * because a name has no separator every project agrees on. Two
340
- * patterns that both cover one name are told apart by how much fixed
341
- * text each states, rather than by narrowing what a hole can cover.
342
- */
343
- /**
344
- * The name a CloudFormation `Fn::Sub` value states. The array form
345
- * takes its template from the first element, and the variable map only
346
- * says where a substitution comes from, which does not change the name.
347
- */
348
- declare function namePatternFromSub(value: unknown): string | null;
349
- /**
350
- * Whether a name says only that somebody else knows it. A wrapper that
351
- * takes its bucket as an argument states `{location.bucket}` and
352
- * nothing more, which says which parameter to ask rather than which
353
- * bucket. A name like that agrees with nothing until something grounds
354
- * it, since otherwise it would agree with every name there is.
355
- */
356
- declare function namesNothing(name: string): boolean;
357
- /** Whether a name has anything a source left for deploy time to fill. */
358
- declare function hasNameHole(name: string): boolean;
359
- /**
360
- * The form two patterns are compared in: every hole reduced to the same
361
- * token, since the two sides pick their own name for the parameter.
362
- */
363
- declare function namePatternKey(name: string): string;
364
- /** How much of a name the writer stated rather than left for deploy time. */
365
- declare function fixedTextLength(name: string): number;
366
- /**
367
- * Whether two names are the same name. Two patterns agree when their
368
- * fixed parts line up, since a hole on one side meets a hole on the
369
- * other. A pattern and a concrete name agree when the fixed parts are
370
- * where the pattern says they are, which is what happens when one side
371
- * hardcoded what the other parameterized.
372
- */
373
- declare function namesAgree(a: string, b: string): boolean;
374
-
375
437
  /**
376
438
  * The suppression pipeline, over the smallest finding both checkers share.
377
439
  *
@@ -622,4 +684,4 @@ declare function metricBinding(opts: {
622
684
  metricType: string | null;
623
685
  }): BoundaryBinding;
624
686
 
625
- export { type BoundaryBehavior, type BoundaryBinding, BoundaryBindingSchema, 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 ParsedChannel, Semantics, type SemconvAttribute, type SemconvMapping, type SourceLocation, SourceLocationSchema, type SuppressableSeverity, type SuppressibleFinding, type SuppressionFile, SuppressionFileSchema, type SuppressionRule, SuppressionRuleSchema, TypeShape, applySuppressionsToFindings, bodyShapesMatch, boundaryKey, boundaryLabel, 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, parseChannel, reportsUnpairedItself, restBinding, ruleBoundaryMatchesKey, runtimeConfigBinding, semanticsAgree, semconvAttributes, servesRequest, storageBinding, validateRule };
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 };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { T as TypeShape, M as MessageBusSemantics, S as Semantics, B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, d as SourceLocationSchema, e as MessageBusTechnology, D as DeployableUnit } from './schemas-CGR6qjFf.js';
3
- export { f as DeployableUnitSchema, F as FunctionCallSemantics, g as FunctionCallSemanticsSchema, G as GraphqlOperationSemantics, h as GraphqlOperationSemanticsSchema, i as GraphqlResolverSemantics, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, R as RestSemantics, l as RestSemanticsSchema, m as RuntimeConfigSemantics, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, p as StorageSemantics, q as StorageSemanticsSchema, r as TypeShapeSchema, s as ecsContainerInstanceName, t as methodsAgree, u as normalizePath, v as routePathAdmits, w as typeDefinitionKey, x as withDefinitionsInlined } from './schemas-CGR6qjFf.js';
2
+ import { T as TypeShape, M as MessageBusSemantics, S as Semantics, B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, d as SourceLocationSchema, e as MessageBusTechnology, D as DeployableUnit } from './schemas-k-169oiY.js';
3
+ export { f as DeployableUnitSchema, F as FunctionCallSemantics, g as FunctionCallSemanticsSchema, G as GraphqlOperationSemantics, h as GraphqlOperationSemanticsSchema, i as GraphqlResolverSemantics, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, R as RestSemantics, l as RestSemanticsSchema, m as RuntimeConfigSemantics, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, p as StorageSemantics, q as StorageSemanticsSchema, r as TypeShapeSchema, s as ecsContainerInstanceName, t as methodsAgree, u as normalizePath, v as pathShape, w as routePathAdmits, x as typeDefinitionKey, y as withDefinitionsInlined } from './schemas-k-169oiY.js';
4
4
 
5
5
  /**
6
6
  * Comparing two TypeShapes structurally.
@@ -243,6 +243,115 @@ declare function reportsUnpairedItself(binding: BoundaryBinding): boolean;
243
243
  */
244
244
  declare function servesRequest(binding: BoundaryBinding, method: string, path: string): MatchResult | null;
245
245
 
246
+ /**
247
+ * The boundary-name syntax, parsed and printed in one place.
248
+ *
249
+ * A name in a summary means one of three things, told apart by the
250
+ * braces alone: a literal (`orders-v1`), a pattern with deploy-time
251
+ * holes (`{stage}-orders-v1`), or a reference saying where to go and
252
+ * ask (`{location.bucket}`, `{ORDER_TABLE}`). A second parser can
253
+ * disagree about which is which, and a second printer can spell a
254
+ * value this one cannot read back, so everything that touches the
255
+ * braces calls this module: `parseBoundaryName` and
256
+ * `boundaryNameString` are the two directions, and the helpers below
257
+ * are views over them. The package README tells the longer story,
258
+ * including why REST route paths and message-bus channels stay apart.
259
+ */
260
+ /** One piece of a pattern: text the writer stated, or a hole. */
261
+ type NamePart = {
262
+ type: "text";
263
+ text: string;
264
+ } | {
265
+ type: "hole";
266
+ label: string;
267
+ };
268
+ /**
269
+ * What a name string means. A `reference` keeps its raw dot-separated
270
+ * path, and `referenceOf` is the view that checks the parts are all
271
+ * present, so a malformed spelling still classifies as a reference and
272
+ * still pairs with nothing.
273
+ */
274
+ type BoundaryName = {
275
+ type: "literal";
276
+ value: string;
277
+ } | {
278
+ type: "pattern";
279
+ parts: NamePart[];
280
+ } | {
281
+ type: "reference";
282
+ path: string[];
283
+ };
284
+ /** What a name string means. The exact inverse of `boundaryNameString`. */
285
+ declare function parseBoundaryName(name: string): BoundaryName;
286
+ /** The one serializer. Everything a summary spells goes through here. */
287
+ declare function boundaryNameString(name: BoundaryName): string;
288
+ /**
289
+ * How a hole is spelled. A reader that assembles a pattern a part at a
290
+ * time, the way the adapter's name reader does, mints each hole here
291
+ * so the spelling cannot drift from the parse above.
292
+ */
293
+ declare function patternHole(label: string): string;
294
+ /**
295
+ * The name a CloudFormation `Fn::Sub` value states. The array form
296
+ * takes its template from the first element, and the variable map only
297
+ * says where a substitution comes from, which does not change the name.
298
+ */
299
+ declare function namePatternFromSub(value: unknown): string | null;
300
+ /**
301
+ * Whether a name says only that somebody else knows it. A wrapper that
302
+ * takes its bucket as an argument states `{location.bucket}` and
303
+ * nothing more, which says which parameter to ask rather than which
304
+ * bucket. A name like that agrees with nothing until something grounds
305
+ * it, since otherwise it would agree with every name there is.
306
+ */
307
+ declare function namesNothing(name: string): boolean;
308
+ /** Whether a name has anything a source left for deploy time to fill. */
309
+ declare function hasNameHole(name: string): boolean;
310
+ /**
311
+ * The form two patterns are compared in: every hole reduced to the same
312
+ * token, since the two sides pick their own name for the parameter.
313
+ */
314
+ declare function namePatternKey(name: string): string;
315
+ /** How much of a name the writer stated rather than left for deploy time. */
316
+ declare function fixedTextLength(name: string): number;
317
+ /**
318
+ * Whether two names are the same name. Two patterns agree when their
319
+ * fixed parts line up, since a hole on one side meets a hole on the
320
+ * other. A pattern and a concrete name agree when the fixed parts are
321
+ * where the pattern says they are, which is what happens when one side
322
+ * hardcoded what the other parameterized. A reference agrees with
323
+ * nothing on either side.
324
+ */
325
+ declare function namesAgree(a: string, b: string): boolean;
326
+ /** Where a reference says to go and ask. */
327
+ interface Reference {
328
+ /**
329
+ * The value the code starts from: a parameter of the unit the
330
+ * reference was written in, or a variable the deployment sets. Which
331
+ * of those it is depends on the unit's inputs, so the reader that
332
+ * grounds a reference decides, not the string.
333
+ */
334
+ root: string;
335
+ /** The fields to read inside it, outermost first. */
336
+ fields: string[];
337
+ }
338
+ /**
339
+ * How a reference is written. Null when a part of it is empty, since a
340
+ * reference has to say what to ask about.
341
+ */
342
+ declare function referenceName(reference: Reference): string | null;
343
+ /**
344
+ * The place a parsed reference says to ask, or null for a name that is
345
+ * not a reference, or for a reference with a part missing, which says
346
+ * nothing anybody could answer.
347
+ */
348
+ declare function referenceOf(name: BoundaryName): Reference | null;
349
+ /**
350
+ * The reference a name states, or null when the name states a name
351
+ * rather than where to go and ask.
352
+ */
353
+ declare function referenceFromName(name: string): Reference | null;
354
+
246
355
  /**
247
356
  * Deciding when two message-bus channel strings mean the same thing.
248
357
  *
@@ -325,53 +434,6 @@ declare function dispatchByType<T extends {
325
434
  type: string;
326
435
  }, R>(table: DispatchTable<T, R>, value: T): R;
327
436
 
328
- /**
329
- * A name two sides state with a hole in it.
330
- *
331
- * A deployed resource is often called something built at deploy time:
332
- * a template writes `!Sub "${StageName}-orders-v1"` and the code writes
333
- * `` `${stage}-orders-v1` ``. Neither side states a string, both agree
334
- * about the fixed text, and each spells the parameter its own way. So a
335
- * name is written here as fixed text with `{}` holes, and two names
336
- * agree when their fixed parts line up.
337
- *
338
- * A hole covers a whole parameter rather than one piece of a name,
339
- * because a name has no separator every project agrees on. Two
340
- * patterns that both cover one name are told apart by how much fixed
341
- * text each states, rather than by narrowing what a hole can cover.
342
- */
343
- /**
344
- * The name a CloudFormation `Fn::Sub` value states. The array form
345
- * takes its template from the first element, and the variable map only
346
- * says where a substitution comes from, which does not change the name.
347
- */
348
- declare function namePatternFromSub(value: unknown): string | null;
349
- /**
350
- * Whether a name says only that somebody else knows it. A wrapper that
351
- * takes its bucket as an argument states `{location.bucket}` and
352
- * nothing more, which says which parameter to ask rather than which
353
- * bucket. A name like that agrees with nothing until something grounds
354
- * it, since otherwise it would agree with every name there is.
355
- */
356
- declare function namesNothing(name: string): boolean;
357
- /** Whether a name has anything a source left for deploy time to fill. */
358
- declare function hasNameHole(name: string): boolean;
359
- /**
360
- * The form two patterns are compared in: every hole reduced to the same
361
- * token, since the two sides pick their own name for the parameter.
362
- */
363
- declare function namePatternKey(name: string): string;
364
- /** How much of a name the writer stated rather than left for deploy time. */
365
- declare function fixedTextLength(name: string): number;
366
- /**
367
- * Whether two names are the same name. Two patterns agree when their
368
- * fixed parts line up, since a hole on one side meets a hole on the
369
- * other. A pattern and a concrete name agree when the fixed parts are
370
- * where the pattern says they are, which is what happens when one side
371
- * hardcoded what the other parameterized.
372
- */
373
- declare function namesAgree(a: string, b: string): boolean;
374
-
375
437
  /**
376
438
  * The suppression pipeline, over the smallest finding both checkers share.
377
439
  *
@@ -622,4 +684,4 @@ declare function metricBinding(opts: {
622
684
  metricType: string | null;
623
685
  }): BoundaryBinding;
624
686
 
625
- export { type BoundaryBehavior, type BoundaryBinding, BoundaryBindingSchema, 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 ParsedChannel, Semantics, type SemconvAttribute, type SemconvMapping, type SourceLocation, SourceLocationSchema, type SuppressableSeverity, type SuppressibleFinding, type SuppressionFile, SuppressionFileSchema, type SuppressionRule, SuppressionRuleSchema, TypeShape, applySuppressionsToFindings, bodyShapesMatch, boundaryKey, boundaryLabel, 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, parseChannel, reportsUnpairedItself, restBinding, ruleBoundaryMatchesKey, runtimeConfigBinding, semanticsAgree, semconvAttributes, servesRequest, storageBinding, validateRule };
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 };
package/dist/index.js CHANGED
@@ -29,11 +29,12 @@ import {
29
29
  metricIdentityKey,
30
30
  normalizePath,
31
31
  parseChannel,
32
+ pathShape,
32
33
  routePathAdmits,
33
34
  semconvMappingOf,
34
35
  typeDefinitionKey,
35
36
  withDefinitionsInlined
36
- } from "./chunk-XUXN6DIX.js";
37
+ } from "./chunk-AQYS5AC6.js";
37
38
 
38
39
  // src/semconv.ts
39
40
  function semconvAttributes(binding) {
@@ -106,29 +107,47 @@ function servesRequest(binding, method, path) {
106
107
  return serves(binding.semantics, method, path);
107
108
  }
108
109
 
109
- // src/codeScope.ts
110
- function codeScopePath(raw) {
111
- return raw.trim().replace(/^\.\//, "").replace(/\/+$/, "");
112
- }
113
- function fileInCodeScope(file, scope) {
114
- const prefix = codeScopePath(scope);
115
- if (prefix === "" || prefix === ".") {
116
- return true;
117
- }
118
- const path = codeScopePath(file);
119
- return path === prefix || path.startsWith(`${prefix}/`);
120
- }
121
-
122
110
  // src/dispatch.ts
123
111
  function dispatchByType(table, value) {
124
112
  const handler = table[value.type];
125
113
  return handler(value);
126
114
  }
127
115
 
128
- // src/namePattern.ts
129
- var HOLE = /\{[^}]*\}/g;
116
+ // src/boundaryName.ts
117
+ var HOLE_SPLIT = /(\{[^}]*\})/;
130
118
  var SUB_TOKEN = /\$\{([^}]*)\}/g;
131
119
  var SUB_ESCAPE = /\$\{!/;
120
+ function isHole(piece) {
121
+ return piece.startsWith("{") && piece.endsWith("}");
122
+ }
123
+ function parseBoundaryName(name) {
124
+ const parts = name.split(HOLE_SPLIT).filter((piece) => piece !== "").map(
125
+ (piece) => isHole(piece) ? { type: "hole", label: piece.slice(1, -1) } : { type: "text", text: piece }
126
+ );
127
+ const only = parts.length === 1 ? parts[0] : void 0;
128
+ if (only !== void 0 && only.type === "hole") {
129
+ return { type: "reference", path: only.label.split(".") };
130
+ }
131
+ if (parts.some((part) => part.type === "hole")) {
132
+ return { type: "pattern", parts };
133
+ }
134
+ return { type: "literal", value: name };
135
+ }
136
+ var partString = {
137
+ text: (part) => part.text,
138
+ hole: (part) => patternHole(part.label)
139
+ };
140
+ var nameString = {
141
+ literal: (name) => name.value,
142
+ pattern: (name) => name.parts.map((part) => dispatchByType(partString, part)).join(""),
143
+ reference: (name) => patternHole(name.path.join("."))
144
+ };
145
+ function boundaryNameString(name) {
146
+ return dispatchByType(nameString, name);
147
+ }
148
+ function patternHole(label) {
149
+ return `{${label}}`;
150
+ }
132
151
  function namePatternFromSub(value) {
133
152
  if (Array.isArray(value)) {
134
153
  return typeof value[0] === "string" ? namePatternFromSub(value[0]) : null;
@@ -139,56 +158,101 @@ function namePatternFromSub(value) {
139
158
  if (SUB_ESCAPE.test(value)) {
140
159
  return null;
141
160
  }
142
- return value.replace(SUB_TOKEN, (_whole, inner) => `{${inner}}`);
161
+ return value.replace(
162
+ SUB_TOKEN,
163
+ (_whole, inner) => patternHole(inner)
164
+ );
143
165
  }
144
166
  function namesNothing(name) {
145
- return /^\{[^}]*\}$/.test(name);
167
+ return parseBoundaryName(name).type === "reference";
146
168
  }
147
169
  function hasNameHole(name) {
148
- HOLE.lastIndex = 0;
149
- return HOLE.test(name);
170
+ return parseBoundaryName(name).type !== "literal";
150
171
  }
151
172
  function namePatternKey(name) {
152
- return name.replace(HOLE, "{}");
173
+ return dispatchByType(
174
+ {
175
+ literal: (parsed) => parsed.value,
176
+ pattern: (parsed) => boundaryNameString({
177
+ type: "pattern",
178
+ parts: parsed.parts.map(
179
+ (part) => part.type === "hole" ? { type: "hole", label: "" } : part
180
+ )
181
+ }),
182
+ reference: () => patternHole("")
183
+ },
184
+ parseBoundaryName(name)
185
+ );
153
186
  }
154
187
  function fixedTextLength(name) {
155
- return name.replace(HOLE, "").length;
156
- }
157
- function isHole(part) {
158
- return part.startsWith("{") && part.endsWith("}");
188
+ return dispatchByType(
189
+ {
190
+ literal: (parsed) => parsed.value.length,
191
+ pattern: (parsed) => parsed.parts.reduce(
192
+ (total, part) => total + (part.type === "text" ? part.text.length : 0),
193
+ 0
194
+ ),
195
+ reference: () => 0
196
+ },
197
+ parseBoundaryName(name)
198
+ );
159
199
  }
160
200
  function quote(text) {
161
201
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
202
  }
163
- function holeSource(_following) {
164
- return ".+";
165
- }
166
203
  function admits(pattern, name) {
167
- const parts = pattern.split(/(\{[^}]*\})/);
168
- const source = parts.map(
169
- (part, index) => isHole(part) ? holeSource(parts[index + 1] ?? "") : quote(part)
170
- ).join("");
204
+ const source = pattern.parts.map((part) => part.type === "hole" ? ".+" : quote(part.text)).join("");
171
205
  return new RegExp(`^${source}$`).test(name);
172
206
  }
173
207
  function namesAgree(a, b) {
174
- if (namesNothing(a) || namesNothing(b)) {
208
+ const left = parseBoundaryName(a);
209
+ const right = parseBoundaryName(b);
210
+ if (left.type === "reference" || right.type === "reference") {
175
211
  return false;
176
212
  }
177
- if (a === b) {
178
- return true;
179
- }
180
- const aHasHole = hasNameHole(a);
181
- const bHasHole = hasNameHole(b);
182
- if (aHasHole && bHasHole) {
213
+ if (left.type === "pattern" && right.type === "pattern") {
183
214
  return namePatternKey(a) === namePatternKey(b);
184
215
  }
185
- if (aHasHole) {
186
- return admits(a, b);
216
+ if (left.type === "pattern") {
217
+ return admits(left, b);
187
218
  }
188
- if (bHasHole) {
189
- return admits(b, a);
219
+ if (right.type === "pattern") {
220
+ return admits(right, a);
190
221
  }
191
- return false;
222
+ return a === b;
223
+ }
224
+ function referenceName(reference) {
225
+ const path = [reference.root, ...reference.fields];
226
+ if (path.some((part) => part === "")) {
227
+ return null;
228
+ }
229
+ return boundaryNameString({ type: "reference", path });
230
+ }
231
+ function referenceOf(name) {
232
+ if (name.type !== "reference") {
233
+ return null;
234
+ }
235
+ const root = name.path[0];
236
+ if (root === void 0 || name.path.some((part) => part === "")) {
237
+ return null;
238
+ }
239
+ return { root, fields: name.path.slice(1) };
240
+ }
241
+ function referenceFromName(name) {
242
+ return referenceOf(parseBoundaryName(name));
243
+ }
244
+
245
+ // src/codeScope.ts
246
+ function codeScopePath(raw) {
247
+ return raw.trim().replace(/^\.\//, "").replace(/\/+$/, "");
248
+ }
249
+ function fileInCodeScope(file, scope) {
250
+ const prefix = codeScopePath(scope);
251
+ if (prefix === "" || prefix === ".") {
252
+ return true;
253
+ }
254
+ const path = codeScopePath(file);
255
+ return path === prefix || path.startsWith(`${prefix}/`);
192
256
  }
193
257
 
194
258
  // src/suppressions.ts
@@ -590,6 +654,7 @@ export {
590
654
  bodyShapesMatch,
591
655
  boundaryKey,
592
656
  boundaryLabel,
657
+ boundaryNameString,
593
658
  busIdentityKey,
594
659
  busesAgree,
595
660
  canPair,
@@ -622,7 +687,13 @@ export {
622
687
  normalizeRuleBoundary,
623
688
  packageExportBinding,
624
689
  pairingKey,
690
+ parseBoundaryName,
625
691
  parseChannel,
692
+ pathShape,
693
+ patternHole,
694
+ referenceFromName,
695
+ referenceName,
696
+ referenceOf,
626
697
  reportsUnpairedItself,
627
698
  restBinding,
628
699
  routePathAdmits,
@@ -173,6 +173,14 @@ type RestSemantics = z.infer<typeof RestSemanticsSchema>;
173
173
  * - Lowercases the static segments (params stay case-sensitive)
174
174
  */
175
175
  declare function normalizePath(path: string): string;
176
+ /**
177
+ * A path with every parameter reduced to its position. `/users/{id}`
178
+ * and `/users/:userId` both come out `/users/{}`, which is the set of
179
+ * requests each one serves, and what deciding whether two sides
180
+ * describe one endpoint rests on. The name a parameter is written
181
+ * under is worth keeping in a report and worth nothing in a comparison.
182
+ */
183
+ declare function pathShape(path: string): string;
176
184
  /**
177
185
  * Whether two REST methods mean the same thing. Equal methods agree,
178
186
  * and `"*"` agrees with any stated method, because a handler that
@@ -443,4 +451,4 @@ declare const TypeShapeSchema: z.ZodType<TypeShape>;
443
451
  */
444
452
  declare function withDefinitionsInlined(shape: TypeShape, definitions: Record<string, TypeShape> | undefined, depth?: number, inProgress?: ReadonlySet<string>): TypeShape;
445
453
 
446
- export { BoundaryBindingSchema as B, ConfidenceInfoSchema as C, type DeployableUnit as D, type FunctionCallSemantics as F, type GraphqlOperationSemantics as G, type MessageBusSemantics as M, type RestSemantics as R, type Semantics as S, type TypeShape as T, ConfidenceLevelSchema as a, ConfidenceSourceSchema as b, CorroborationSchema as c, SourceLocationSchema as d, type MessageBusTechnology as e, DeployableUnitSchema as f, FunctionCallSemanticsSchema as g, GraphqlOperationSemanticsSchema as h, type GraphqlResolverSemantics as i, GraphqlResolverSemanticsSchema as j, MessageBusSemanticsSchema as k, RestSemanticsSchema as l, type RuntimeConfigSemantics as m, RuntimeConfigSemanticsSchema as n, SemanticsSchema as o, type StorageSemantics as p, StorageSemanticsSchema as q, TypeShapeSchema as r, ecsContainerInstanceName as s, methodsAgree as t, normalizePath as u, routePathAdmits as v, typeDefinitionKey as w, withDefinitionsInlined as x };
454
+ export { BoundaryBindingSchema as B, ConfidenceInfoSchema as C, type DeployableUnit as D, type FunctionCallSemantics as F, type GraphqlOperationSemantics as G, type MessageBusSemantics as M, type RestSemantics as R, type Semantics as S, type TypeShape as T, ConfidenceLevelSchema as a, ConfidenceSourceSchema as b, CorroborationSchema as c, SourceLocationSchema as d, type MessageBusTechnology as e, DeployableUnitSchema as f, FunctionCallSemanticsSchema as g, GraphqlOperationSemanticsSchema as h, type GraphqlResolverSemantics as i, GraphqlResolverSemanticsSchema as j, MessageBusSemanticsSchema as k, RestSemanticsSchema as l, type RuntimeConfigSemantics as m, RuntimeConfigSemanticsSchema as n, SemanticsSchema as o, type StorageSemantics as p, StorageSemanticsSchema as q, TypeShapeSchema as r, ecsContainerInstanceName as s, methodsAgree as t, normalizePath as u, pathShape as v, routePathAdmits as w, typeDefinitionKey as x, withDefinitionsInlined as y };
@@ -173,6 +173,14 @@ type RestSemantics = z.infer<typeof RestSemanticsSchema>;
173
173
  * - Lowercases the static segments (params stay case-sensitive)
174
174
  */
175
175
  declare function normalizePath(path: string): string;
176
+ /**
177
+ * A path with every parameter reduced to its position. `/users/{id}`
178
+ * and `/users/:userId` both come out `/users/{}`, which is the set of
179
+ * requests each one serves, and what deciding whether two sides
180
+ * describe one endpoint rests on. The name a parameter is written
181
+ * under is worth keeping in a report and worth nothing in a comparison.
182
+ */
183
+ declare function pathShape(path: string): string;
176
184
  /**
177
185
  * Whether two REST methods mean the same thing. Equal methods agree,
178
186
  * and `"*"` agrees with any stated method, because a handler that
@@ -443,4 +451,4 @@ declare const TypeShapeSchema: z.ZodType<TypeShape>;
443
451
  */
444
452
  declare function withDefinitionsInlined(shape: TypeShape, definitions: Record<string, TypeShape> | undefined, depth?: number, inProgress?: ReadonlySet<string>): TypeShape;
445
453
 
446
- export { BoundaryBindingSchema as B, ConfidenceInfoSchema as C, type DeployableUnit as D, type FunctionCallSemantics as F, type GraphqlOperationSemantics as G, type MessageBusSemantics as M, type RestSemantics as R, type Semantics as S, type TypeShape as T, ConfidenceLevelSchema as a, ConfidenceSourceSchema as b, CorroborationSchema as c, SourceLocationSchema as d, type MessageBusTechnology as e, DeployableUnitSchema as f, FunctionCallSemanticsSchema as g, GraphqlOperationSemanticsSchema as h, type GraphqlResolverSemantics as i, GraphqlResolverSemanticsSchema as j, MessageBusSemanticsSchema as k, RestSemanticsSchema as l, type RuntimeConfigSemantics as m, RuntimeConfigSemanticsSchema as n, SemanticsSchema as o, type StorageSemantics as p, StorageSemanticsSchema as q, TypeShapeSchema as r, ecsContainerInstanceName as s, methodsAgree as t, normalizePath as u, routePathAdmits as v, typeDefinitionKey as w, withDefinitionsInlined as x };
454
+ export { BoundaryBindingSchema as B, ConfidenceInfoSchema as C, type DeployableUnit as D, type FunctionCallSemantics as F, type GraphqlOperationSemantics as G, type MessageBusSemantics as M, type RestSemantics as R, type Semantics as S, type TypeShape as T, ConfidenceLevelSchema as a, ConfidenceSourceSchema as b, CorroborationSchema as c, SourceLocationSchema as d, type MessageBusTechnology as e, DeployableUnitSchema as f, FunctionCallSemanticsSchema as g, GraphqlOperationSemanticsSchema as h, type GraphqlResolverSemantics as i, GraphqlResolverSemanticsSchema as j, MessageBusSemanticsSchema as k, RestSemanticsSchema as l, type RuntimeConfigSemantics as m, RuntimeConfigSemanticsSchema as n, SemanticsSchema as o, type StorageSemantics as p, StorageSemanticsSchema as q, TypeShapeSchema as r, ecsContainerInstanceName as s, methodsAgree as t, normalizePath as u, pathShape as v, routePathAdmits as w, typeDefinitionKey as x, withDefinitionsInlined as y };
package/dist/schemas.cjs CHANGED
@@ -373,6 +373,9 @@ function normalizePath(path) {
373
373
  );
374
374
  return normalized;
375
375
  }
376
+ function pathShape(path) {
377
+ return normalizePath(path).replace(/\{[^}]*\}/g, "{}");
378
+ }
376
379
  function methodsAgree(a, b) {
377
380
  if (a === null || b === null) {
378
381
  return false;
@@ -424,12 +427,17 @@ var restSemantics = defineBoundarySemantics({
424
427
  * settles it inside the bucket. The identity key stays what a reader
425
428
  * sees and a suppression targets ("GET /users", "* /users"), and
426
429
  * this key groups what a reader still tells apart.
430
+ *
431
+ * A parameter's name goes too. A spec written by hand says
432
+ * `{userId}` where the Express route that serves it says `:id`, and
433
+ * both match the same requests, so pairing on the name gave two
434
+ * boundaries that never met and a run with nothing to report.
427
435
  */
428
436
  pairingKey(semantics) {
429
437
  if (semantics.method === null || semantics.path === null) {
430
438
  return null;
431
439
  }
432
- return `rest ${normalizePath(semantics.path)}`;
440
+ return `rest ${pathShape(semantics.path)}`;
433
441
  },
434
442
  sidesAgree(a, b) {
435
443
  return methodsAgree(a.method, b.method);
@@ -535,7 +543,9 @@ var StorageSemanticsSchema = import_zod9.z.object({
535
543
  * The table, bucket, collection, or index, as the source declares
536
544
  * it, or null when the source states one this reader could not
537
545
  * settle. A null container pairs with nothing, rather than with
538
- * whatever its source text happens to spell.
546
+ * whatever its source text happens to spell. The string is in the
547
+ * boundary-name syntax: a literal, a pattern with `{}` holes, or a
548
+ * reference, and `parseBoundaryName` is the one reader of it.
539
549
  */
540
550
  container: import_zod9.z.string().nullable(),
541
551
  /**
@@ -1,2 +1,2 @@
1
1
  import 'zod';
2
- export { B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, f as DeployableUnitSchema, g as FunctionCallSemanticsSchema, h as GraphqlOperationSemanticsSchema, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, l as RestSemanticsSchema, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, d as SourceLocationSchema, q as StorageSemanticsSchema, T as TypeShape, r as TypeShapeSchema, w as typeDefinitionKey, x as withDefinitionsInlined } from './schemas-CGR6qjFf.cjs';
2
+ export { B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, f as DeployableUnitSchema, g as FunctionCallSemanticsSchema, h as GraphqlOperationSemanticsSchema, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, l as RestSemanticsSchema, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, d as SourceLocationSchema, q as StorageSemanticsSchema, T as TypeShape, r as TypeShapeSchema, x as typeDefinitionKey, y as withDefinitionsInlined } from './schemas-k-169oiY.cjs';
package/dist/schemas.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import 'zod';
2
- export { B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, f as DeployableUnitSchema, g as FunctionCallSemanticsSchema, h as GraphqlOperationSemanticsSchema, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, l as RestSemanticsSchema, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, d as SourceLocationSchema, q as StorageSemanticsSchema, T as TypeShape, r as TypeShapeSchema, w as typeDefinitionKey, x as withDefinitionsInlined } from './schemas-CGR6qjFf.js';
2
+ export { B as BoundaryBindingSchema, C as ConfidenceInfoSchema, a as ConfidenceLevelSchema, b as ConfidenceSourceSchema, c as CorroborationSchema, f as DeployableUnitSchema, g as FunctionCallSemanticsSchema, h as GraphqlOperationSemanticsSchema, j as GraphqlResolverSemanticsSchema, k as MessageBusSemanticsSchema, l as RestSemanticsSchema, n as RuntimeConfigSemanticsSchema, o as SemanticsSchema, d as SourceLocationSchema, q as StorageSemanticsSchema, T as TypeShape, r as TypeShapeSchema, x as typeDefinitionKey, y as withDefinitionsInlined } from './schemas-k-169oiY.js';
package/dist/schemas.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  TypeShapeSchema,
18
18
  typeDefinitionKey,
19
19
  withDefinitionsInlined
20
- } from "./chunk-XUXN6DIX.js";
20
+ } from "./chunk-AQYS5AC6.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.9.0",
3
+ "version": "0.11.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": [