@suss/ir-core 0.34.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schemas.cjs CHANGED
@@ -449,6 +449,38 @@ function pathAfterOrigin(text) {
449
449
  const cut = withoutOrigin.search(/[?#]/);
450
450
  return cut === -1 ? withoutOrigin : withoutOrigin.slice(0, cut);
451
451
  }
452
+ var PRIVATE_SUFFIXES = [
453
+ "local",
454
+ "internal",
455
+ "localhost",
456
+ "lan",
457
+ "home.arpa",
458
+ "svc",
459
+ "test",
460
+ "example",
461
+ "invalid",
462
+ "example.com",
463
+ "example.net",
464
+ "example.org"
465
+ ];
466
+ var PRIVATE_IPV4 = /^(?:127\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.|0\.0\.0\.0$)/;
467
+ function hostnameOf(host) {
468
+ const lower = host.toLowerCase();
469
+ const name = lower.startsWith("[") ? lower.slice(0, lower.indexOf("]") + 1) : lower.split(":")[0] ?? lower;
470
+ return name.replace(/\.$/, "");
471
+ }
472
+ function hostCanBeOwn(host) {
473
+ const name = hostnameOf(host);
474
+ if (name === "[::1]" || /^\[f[cd]/.test(name) || PRIVATE_IPV4.test(name)) {
475
+ return true;
476
+ }
477
+ if (!name.includes(".")) {
478
+ return true;
479
+ }
480
+ return PRIVATE_SUFFIXES.some(
481
+ (suffix) => name === suffix || name.endsWith(`.${suffix}`)
482
+ );
483
+ }
452
484
 
453
485
  // src/semantics/pathPattern.ts
454
486
  var ONE = { kind: "one" };
@@ -499,14 +531,18 @@ function shapedItem(segment) {
499
531
  return ".*";
500
532
  }
501
533
  if (part.startsWith("{")) {
502
- return "[^/]+";
534
+ return /[?*]\}$/.test(part) ? "[^/]*" : "[^/]+";
503
535
  }
504
536
  return escapedForRegex(part);
505
537
  }).join("");
538
+ const holes = segment.match(/\{[^}]*\}|\*/g) ?? [];
539
+ const text = segment.replace(/\{[^}]*\}|\*/g, "");
506
540
  return {
507
541
  kind: "shaped",
508
542
  shape: segment.replace(/\{[^}]*\}/g, "{}"),
509
- test: new RegExp(`^${source}$`)
543
+ test: new RegExp(`^${source}$`),
544
+ bare: holes.every((hole) => hole === "*" || /[?*]\}$/.test(hole)) ? text : null,
545
+ textLength: text.length
510
546
  };
511
547
  }
512
548
  function itemsOf(segment) {
@@ -526,10 +562,35 @@ function segmentsOf(alternative) {
526
562
  const trimmed = alternative.startsWith("/") ? alternative.slice(1) : alternative;
527
563
  return trimmed === "" ? [] : trimmed.split("/");
528
564
  }
565
+ function patternSegmentsOf(alternative) {
566
+ const segments = segmentsOf(alternative);
567
+ const first = segments[0];
568
+ const opensWithBaseUrl = !alternative.startsWith("/") && first !== void 0 && HOLE_SEGMENT.test(first);
569
+ return opensWithBaseUrl ? segments.slice(1) : segments;
570
+ }
571
+ function pathAfterBaseUrl(path) {
572
+ const segments = segmentsOf(path);
573
+ const first = segments[0];
574
+ if (path.startsWith("/") || first === void 0 || segments.length < 2 || !HOLE_SEGMENT.test(first)) {
575
+ return path;
576
+ }
577
+ return `/${segments.slice(1).join("/")}`;
578
+ }
579
+ var PATTERNS = /* @__PURE__ */ new Map();
580
+ var PATTERN_CACHE_LIMIT = 5e4;
529
581
  function patternOf(path) {
530
- return alternativesOf(path).map(
531
- (alternative) => segmentsOf(alternative).flatMap(itemsOf)
582
+ const known = PATTERNS.get(path);
583
+ if (known !== void 0) {
584
+ return known;
585
+ }
586
+ const pattern = alternativesOf(path).map(
587
+ (alternative) => patternSegmentsOf(alternative).flatMap(itemsOf)
532
588
  );
589
+ if (PATTERNS.size >= PATTERN_CACHE_LIMIT) {
590
+ PATTERNS.clear();
591
+ }
592
+ PATTERNS.set(path, pattern);
593
+ return pattern;
533
594
  }
534
595
  function literalItemsOf(path) {
535
596
  return segmentsOf(path).map((text) => ({ kind: "text", text }));
@@ -608,17 +669,104 @@ function pathSpansShapes(path) {
608
669
  function countOf(items, kinds) {
609
670
  return items.filter((item) => kinds.includes(item.kind)).length;
610
671
  }
611
- function pathSpecificity(path) {
612
- const alternatives = patternOf(path);
613
- const ranks = alternatives.map((items) => [
672
+ function readingSpecificity(items) {
673
+ return [
614
674
  countOf(items, ["text"]),
615
675
  countOf(items, ["shaped"]),
616
676
  -countOf(items, ["optional", "star"])
617
- ]);
618
- const loosest = ranks.reduce(
619
- (low, rank) => compareRanks(rank, low) < 0 ? rank : low
677
+ ];
678
+ }
679
+ function segmentFit(route, request) {
680
+ if (!meetOnOneSegment(route, request)) {
681
+ return "apart";
682
+ }
683
+ if (request.kind === "text") {
684
+ if (route.kind === "text") {
685
+ return "stated";
686
+ }
687
+ return route.kind === "shaped" ? "shaped" : "free";
688
+ }
689
+ if (absorbsAnything(route)) {
690
+ return "free";
691
+ }
692
+ if (request.kind !== "shaped") {
693
+ return "guess";
694
+ }
695
+ if (route.kind === "shaped") {
696
+ return "free";
697
+ }
698
+ return route.kind === "text" && request.bare === route.text ? "stated" : "guess";
699
+ }
700
+ function shapedText(item) {
701
+ return item.kind === "shaped" ? item.textLength : 0;
702
+ }
703
+ function betterFit(a, b) {
704
+ return a === void 0 || compareRanks(b, a) > 0;
705
+ }
706
+ function bestFit(route, request) {
707
+ const width = request.length + 1;
708
+ const best = new Array((route.length + 1) * width);
709
+ best[0] = [0, 0];
710
+ const offer = (i, j, fit) => {
711
+ const at = i * width + j;
712
+ if (betterFit(best[at], fit)) {
713
+ best[at] = fit;
714
+ }
715
+ };
716
+ for (let reached = 0; reached <= route.length + request.length; reached += 1) {
717
+ for (let i = Math.max(0, reached - request.length); i <= Math.min(route.length, reached); i += 1) {
718
+ const j = reached - i;
719
+ const fit = best[i * width + j];
720
+ if (fit === void 0) {
721
+ continue;
722
+ }
723
+ const left = route[i];
724
+ const right = request[j];
725
+ if (left !== void 0 && skippable(left)) {
726
+ offer(i + 1, j, fit);
727
+ }
728
+ if (right !== void 0 && skippable(right)) {
729
+ offer(i, j + 1, fit);
730
+ }
731
+ if (left === void 0 || right === void 0) {
732
+ continue;
733
+ }
734
+ if (left.kind === "star" && right.kind === "star") {
735
+ continue;
736
+ }
737
+ const kind = segmentFit(left, right);
738
+ if (kind === "apart" || kind === "guess") {
739
+ continue;
740
+ }
741
+ offer(
742
+ left.kind === "star" ? i : i + 1,
743
+ right.kind === "star" ? j : j + 1,
744
+ [
745
+ fit[0] + (kind === "stated" ? 1 : 0),
746
+ fit[1] + (kind === "shaped" ? shapedText(left) : 0)
747
+ ]
748
+ );
749
+ }
750
+ }
751
+ return best[route.length * width + request.length] ?? null;
752
+ }
753
+ function requestRank(route, request) {
754
+ let found = null;
755
+ for (const routeItems of patternOf(route)) {
756
+ for (const requestItems of patternOf(request)) {
757
+ const fit = bestFit(routeItems, requestItems);
758
+ const rank = fit === null ? null : [...fit, ...readingSpecificity(routeItems)];
759
+ if (rank !== null && (found === null || compareRanks(rank, found) > 0)) {
760
+ found = rank;
761
+ }
762
+ }
763
+ }
764
+ return found;
765
+ }
766
+ function isCatchAll(route) {
767
+ return patternOf(route).some(
768
+ (items) => items.length > 0 && items.every(absorbsAnything) && items.some((item) => item.kind === "star")
620
769
  );
621
- return [...loosest, 1 - alternatives.length];
622
770
  }
623
771
  function compareRanks(a, b) {
624
772
  const length = Math.max(a.length, b.length);
@@ -648,7 +796,18 @@ var RestSemanticsSchema = import_zod7.z.object({
648
796
  * here so the pairing layer can see them without unwrapping any
649
797
  * metadata. Absent or empty for an inferred source.
650
798
  */
651
- declaredResponses: import_zod7.z.array(import_zod7.z.number()).optional()
799
+ declaredResponses: import_zod7.z.array(import_zod7.z.number()).optional(),
800
+ /**
801
+ * The host a client wrote in an absolute URL, with its port
802
+ * (`api.example.com:8443`). A piece suss could not read is written
803
+ * `{name}`. Absent for a relative URL and for a route.
804
+ */
805
+ host: import_zod7.z.string().min(1).optional(),
806
+ /**
807
+ * True for a route the framework also serves with `.{format}` after
808
+ * its path, the way Rails serves `/search.json` from `get "search"`.
809
+ */
810
+ optionalFormat: import_zod7.z.boolean().optional()
652
811
  });
653
812
  var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
654
813
  function baseUrlReference(semantics) {
@@ -656,7 +815,10 @@ function baseUrlReference(semantics) {
656
815
  return label === void 0 ? null : referenceFromName(patternHole(label));
657
816
  }
658
817
  function normalizePath(path) {
659
- let normalized = path.replace(/:([a-zA-Z_]\w*)([?+*]?)/g, "{$1$2}");
818
+ let normalized = withoutParameterConstraints(path).replace(
819
+ /:([a-zA-Z_]\w*)([?+*]?)/g,
820
+ "{$1$2}"
821
+ );
660
822
  if (normalized.length > 1 && normalized.endsWith("/")) {
661
823
  normalized = normalized.slice(0, -1);
662
824
  }
@@ -666,6 +828,46 @@ function normalizePath(path) {
666
828
  );
667
829
  return normalized;
668
830
  }
831
+ function withoutParameterConstraints(path) {
832
+ let out = "";
833
+ let i = 0;
834
+ while (i < path.length) {
835
+ const name = /^:[a-zA-Z_]\w*\(/.exec(path.slice(i));
836
+ if (name === null) {
837
+ out += path[i];
838
+ i += 1;
839
+ continue;
840
+ }
841
+ const open = i + name[0].length - 1;
842
+ const close = closingParenthesis(path, open);
843
+ if (close === -1) {
844
+ return out + path.slice(i);
845
+ }
846
+ out += path.slice(i, open);
847
+ i = close + 1;
848
+ }
849
+ return out;
850
+ }
851
+ function closingParenthesis(text, open) {
852
+ let depth = 0;
853
+ for (let i = open; i < text.length; i += 1) {
854
+ const char = text[i];
855
+ if (char === "\\") {
856
+ i += 1;
857
+ continue;
858
+ }
859
+ if (char === "(") {
860
+ depth += 1;
861
+ }
862
+ if (char === ")") {
863
+ depth -= 1;
864
+ if (depth === 0) {
865
+ return i;
866
+ }
867
+ }
868
+ }
869
+ return -1;
870
+ }
669
871
  function pathShape(path) {
670
872
  return normalizePath(path).replace(/\{[^{}]*?([?+*]?)\}/g, "{$1}");
671
873
  }
@@ -678,8 +880,39 @@ function methodsAgree(a, b) {
678
880
  function routePathAdmits(declaredPath, requestPath) {
679
881
  return patternAdmits(normalizePath(declaredPath), normalizePath(requestPath));
680
882
  }
681
- function routePathsMeet(a, b) {
682
- return pathsMeet(normalizePath(a), normalizePath(b));
883
+ function readHost(semantics) {
884
+ const host = semantics.host;
885
+ return host === void 0 || /[{(]/.test(host) ? void 0 : host;
886
+ }
887
+ function hostsAgree(a, b) {
888
+ return readHost(a) === readHost(b);
889
+ }
890
+ function hostUnread(semantics) {
891
+ if (semantics.host !== void 0) {
892
+ return readHost(semantics) === void 0;
893
+ }
894
+ return baseUrlReference(semantics) !== null;
895
+ }
896
+ function servedPath(semantics, path) {
897
+ const normalized = normalizePath(path);
898
+ return semantics.optionalFormat === true ? `${normalized}(|.{format})` : normalized;
899
+ }
900
+ function withoutHost(semantics) {
901
+ const { host: _host, ...rest } = semantics;
902
+ return rest;
903
+ }
904
+ function groundedBaseUrl(semantics, deployment) {
905
+ const reference = baseUrlReference(semantics);
906
+ if (reference === null) {
907
+ return null;
908
+ }
909
+ const base = deployment.setTo(reference);
910
+ if (base === null) {
911
+ return null;
912
+ }
913
+ const rest = OPENING_HOLE.exec(semantics.path ?? "")?.[2] ?? "";
914
+ const grounded = pathAfterOrigin(`${base}${rest}`);
915
+ return { ...semantics, path: grounded === "" ? "/" : grounded };
683
916
  }
684
917
  var restSemantics = defineBoundarySemantics({
685
918
  name: "rest",
@@ -719,19 +952,46 @@ var restSemantics = defineBoundarySemantics({
719
952
  if (semantics.method === null || semantics.path === null) {
720
953
  return null;
721
954
  }
722
- return `rest ${pathShape(semantics.path)}`;
955
+ const shape = pathShape(pathAfterBaseUrl(normalizePath(semantics.path)));
956
+ const host = readHost(semantics);
957
+ return host === void 0 ? `rest ${shape}` : `rest //${host}${shape}`;
958
+ },
959
+ /**
960
+ * A spec writes `/teams/{team_id}` where the route that serves it
961
+ * writes `/teams/:id`. The method stays in, so a `"*"` handler is
962
+ * never compared with the document for one method.
963
+ */
964
+ operationKey(semantics) {
965
+ if (semantics.method === null || semantics.path === null) {
966
+ return null;
967
+ }
968
+ return `${semantics.method.toUpperCase()} ${pathShape(semantics.path)}`;
723
969
  },
724
970
  sidesAgree(a, b) {
725
- return methodsAgree(a.method, b.method);
971
+ return methodsAgree(a.method, b.method) && hostsAgree(a, b);
726
972
  },
727
973
  spansBuckets(semantics) {
728
- return semantics.path !== null && pathSpansShapes(normalizePath(semantics.path));
974
+ return semantics.path !== null && (semantics.optionalFormat === true || pathSpansShapes(normalizePath(semantics.path)));
729
975
  },
730
976
  bucketsMeet(a, b) {
731
- return a.path !== null && b.path !== null && routePathsMeet(a.path, b.path);
977
+ return a.path !== null && b.path !== null && hostsAgree(a, b) && pathsMeet(servedPath(a, a.path), servedPath(b, b.path));
732
978
  },
733
- bucketRank(semantics) {
734
- return semantics.path === null ? [] : pathSpecificity(normalizePath(semantics.path));
979
+ /**
980
+ * A catch-all route never takes a call whose host suss could not
981
+ * read, since nothing else ties that call to this app.
982
+ */
983
+ pairRank(provider, consumer) {
984
+ if (provider.path === null || consumer.path === null) {
985
+ return [];
986
+ }
987
+ const route = normalizePath(provider.path);
988
+ if (hostUnread(consumer) && isCatchAll(route)) {
989
+ return null;
990
+ }
991
+ return requestRank(
992
+ servedPath(provider, provider.path),
993
+ normalizePath(consumer.path)
994
+ );
735
995
  },
736
996
  /**
737
997
  * The identity key, with a missing half still readable: `ANY` when
@@ -772,27 +1032,29 @@ var restSemantics = defineBoundarySemantics({
772
1032
  },
773
1033
  /**
774
1034
  * A call whose base URL the deployment fills in, resolved to the
775
- * path it reaches.
1035
+ * path it reaches, and a call to a host that can be the project's
1036
+ * own, with the host left out so it meets the route.
776
1037
  *
777
- * The source cannot settle this on its own, so the adapter leaves
778
- * the hole in. `API_BASE` could be `http://backend.internal`, and
779
- * then the path is `/orders`. It could equally be `/api/v2`, and
1038
+ * The source cannot settle a base URL on its own, so the adapter
1039
+ * leaves the hole in. `API_BASE` could be `http://backend.internal`,
1040
+ * and then the path is `/orders`. It could equally be `/api/v2`, and
780
1041
  * then the path is `/api/v2/orders`. Filling in the deployed value
781
- * and reading the path back out gets the right one either way.
782
- * `baseUrlReference` finds the hole that is the base URL.
1042
+ * and reading the path back out gets the right one either way, and
1043
+ * a base the deployment sets counts as the app's own.
783
1044
  */
784
- groundName(semantics, deployment) {
785
- const reference = baseUrlReference(semantics);
786
- if (reference === null) {
787
- return null;
788
- }
789
- const base = deployment.setTo(reference);
790
- if (base === null) {
1045
+ /** A listed host matches with or without the port the call gives. */
1046
+ withoutOwnHost(semantics, ownHosts) {
1047
+ const host = semantics.host?.toLowerCase();
1048
+ if (host === void 0) {
791
1049
  return null;
792
1050
  }
793
- const rest = OPENING_HOLE.exec(semantics.path ?? "")?.[2] ?? "";
794
- const grounded = pathAfterOrigin(`${base}${rest}`);
795
- return { ...semantics, path: grounded === "" ? "/" : grounded };
1051
+ return ownHosts.has(host) || ownHosts.has(host.replace(/:\d+$/, "")) ? withoutHost(semantics) : null;
1052
+ },
1053
+ groundName(semantics, deployment) {
1054
+ const based = groundedBaseUrl(semantics, deployment);
1055
+ const grounded = based ?? semantics;
1056
+ const host = readHost(grounded);
1057
+ return host !== void 0 && hostCanBeOwn(host) ? withoutHost(grounded) : based;
796
1058
  },
797
1059
  nameReference: baseUrlReference,
798
1060
  ruleBoundary: {
@@ -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, I as typeDefinitionKey, J as withDefinitionsInlined } from './schemas-CghZNwSE.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, I as typeDefinitionKey, J as withDefinitionsInlined } from './schemas-lfg8kvak.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, I as typeDefinitionKey, J as withDefinitionsInlined } from './schemas-CghZNwSE.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, I as typeDefinitionKey, J as withDefinitionsInlined } from './schemas-lfg8kvak.js';
package/dist/schemas.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  TypeShapeSchema,
18
18
  typeDefinitionKey,
19
19
  withDefinitionsInlined
20
- } from "./chunk-WOXA5GBL.js";
20
+ } from "./chunk-A46EUIKC.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.34.0",
3
+ "version": "0.34.1",
4
4
  "type": "module",
5
5
  "description": "The type primitives every suss summary is built from: type shapes, boundary bindings, source locations and confidence.",
6
6
  "keywords": [