@suss/packs 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/rails.js CHANGED
@@ -1,22 +1,26 @@
1
1
  // ../framework/rails/dist/index.js
2
- import fs4 from "fs";
3
- import path4 from "path";
4
- import { z } from "zod";
2
+ import fs5 from "fs";
3
+ import path5 from "path";
4
+ import { z as z15 } from "zod";
5
5
  import { underscoreConstantPath as underscoreConstantPath2 } from "@suss/adapter-ruby";
6
6
  import fs from "fs";
7
7
  import path from "path";
8
+ import fs2 from "fs";
9
+ import path2 from "path";
8
10
  import {
9
11
  bodyStatements,
12
+ children,
10
13
  field,
11
14
  parseRubySync,
12
15
  underscoreConstantPath
13
16
  } from "@suss/adapter-ruby";
14
- import fs2 from "fs";
15
- import path2 from "path";
16
17
  import fs3 from "fs";
17
18
  import path3 from "path";
19
+ import fs4 from "fs";
20
+ import path4 from "path";
18
21
  import {
19
22
  bodyStatements as bodyStatements2,
23
+ children as children2,
20
24
  field as field2,
21
25
  hashKeySymbolName,
22
26
  nestedStatements,
@@ -24,6 +28,1446 @@ import {
24
28
  stringValueOf,
25
29
  symbolValue
26
30
  } from "@suss/adapter-ruby";
31
+
32
+ // ../ir-core/dist/chunk-A46EUIKC.js
33
+ import { z as z13 } from "zod";
34
+ import { z as z12 } from "zod";
35
+ import { z } from "zod";
36
+ import { z as z2 } from "zod";
37
+ import { z as z3 } from "zod";
38
+ import { z as z4 } from "zod";
39
+ import { z as z5 } from "zod";
40
+ import { z as z6 } from "zod";
41
+ import { z as z7 } from "zod";
42
+ import { z as z9 } from "zod";
43
+ import { z as z8 } from "zod";
44
+ import { z as z10 } from "zod";
45
+ import { z as z11 } from "zod";
46
+ function gqlIdentityKey(typeName, fieldName) {
47
+ return `gql:${typeName}.${fieldName}`;
48
+ }
49
+ function fnIdentityKey(packageName, exportPath) {
50
+ return `fn:${packageName}::${exportPath.join(".")}`;
51
+ }
52
+ function busIdentityKey(messageBus, subject) {
53
+ return `bus:${messageBus} ${subject}`;
54
+ }
55
+ function unitIdentityKey(deploymentTarget, instanceName) {
56
+ return `unit:${deploymentTarget} ${instanceName}`;
57
+ }
58
+ function metricIdentityKey(metricSystem, metricType) {
59
+ return `metric:${metricSystem} ${metricType}`;
60
+ }
61
+ function ioIdentityKey(target) {
62
+ return `io:${target}`;
63
+ }
64
+ function defineBoundarySemantics(definition) {
65
+ return definition;
66
+ }
67
+ var FunctionCallSemanticsSchema = z.object({
68
+ name: z.literal("function-call"),
69
+ /**
70
+ * Where the function lives inside this repository. It is either the
71
+ * name of a module the project lists in `suss.json`, such as
72
+ * `"billing"`, or a module path, such as the file a server action is
73
+ * written in. A name has no separator in it and a path always does.
74
+ * Packs that don't do cross-module pairing can leave it unset.
75
+ */
76
+ module: z.string().optional(),
77
+ /** Named export within the module, when applicable. */
78
+ exportName: z.string().optional(),
79
+ /**
80
+ * Package name, as written in `package.json`, when this identity
81
+ * refers to a public package export, for example
82
+ * `"@suss/behavioral-ir"`. Packs that resolve a package's public
83
+ * surface (the `packageExports` discovery variant) set it alongside
84
+ * `exportPath`. It is different from `module`, which is a
85
+ * repo-relative module path used for pairing inside one repo.
86
+ */
87
+ package: z.string().optional(),
88
+ /**
89
+ * Path to the exported binding within the package, starting with the
90
+ * sub-path key when one is used. Examples:
91
+ * `["parseSummary"]`: a root export
92
+ * `["schemas", "BoundaryBindingSchema"]`: the sub-path `./schemas`
93
+ *
94
+ * The first segment is the sub-path without the leading `./`, and
95
+ * `"."` is left out. The last segment is the exported name.
96
+ */
97
+ exportPath: z.array(z.string()).optional()
98
+ });
99
+ function isModuleName(module) {
100
+ return !module.includes("/") && !module.includes("\\");
101
+ }
102
+ var functionCallSemantics = defineBoundarySemantics({
103
+ name: "function-call",
104
+ schema: FunctionCallSemanticsSchema,
105
+ // A call inside the process does not produce a span, and the
106
+ // conventions have no attributes for it.
107
+ semconv: {},
108
+ behavior: {
109
+ /** A call returns a value, with no status. */
110
+ exchangesHttpResponses: false,
111
+ leavesTheProcess: false,
112
+ reportsUnpairedItself: false,
113
+ /**
114
+ * In-repo units such as components have no key on purpose. They
115
+ * pair through call edges instead of the keyed pass, so a missing
116
+ * key does not make them unreachable.
117
+ */
118
+ canPair: () => true,
119
+ rewritePaths(semantics, rewrite) {
120
+ if (semantics.module === void 0 || isModuleName(semantics.module)) {
121
+ return semantics;
122
+ }
123
+ return { ...semantics, module: rewrite(semantics.module) };
124
+ },
125
+ /**
126
+ * `"fn:<package>::<exportPath>"` when both are set, and
127
+ * `"fn:<module>::<exportName>"` for an in-repo unit that gives its
128
+ * module, as a server action does. Components and plain handlers
129
+ * give neither and have no key.
130
+ */
131
+ identityKey(semantics) {
132
+ if (semantics.package !== void 0 && semantics.exportPath !== void 0 && semantics.exportPath.length > 0) {
133
+ return fnIdentityKey(semantics.package, semantics.exportPath);
134
+ }
135
+ if (semantics.module !== void 0 && semantics.exportName !== void 0) {
136
+ return fnIdentityKey(semantics.module, [semantics.exportName]);
137
+ }
138
+ return null;
139
+ }
140
+ }
141
+ });
142
+ var GraphqlOperationSemanticsSchema = z2.object({
143
+ name: z2.literal("graphql-operation"),
144
+ /** Optional operation name. An anonymous query or mutation leaves it unset. */
145
+ operationName: z2.string().optional(),
146
+ operationType: z2.enum(["query", "mutation", "subscription"])
147
+ });
148
+ var graphqlOperationSemantics = defineBoundarySemantics({
149
+ name: "graphql-operation",
150
+ schema: GraphqlOperationSemanticsSchema,
151
+ semconv: {
152
+ operationName: { name: "graphql.operation.name" },
153
+ operationType: { name: "graphql.operation.type" }
154
+ },
155
+ behavior: {
156
+ /**
157
+ * An operation gets a data-and-errors document back, which the
158
+ * GraphQL contract checker reads. Status codes do not apply.
159
+ */
160
+ exchangesHttpResponses: false,
161
+ leavesTheProcess: true,
162
+ reportsUnpairedItself: false,
163
+ identityKey: () => null,
164
+ /** No key on purpose. The dedicated pass pairs an operation by its document. */
165
+ canPair: () => true,
166
+ /** `query GetUser`. Only for display, since the contract checker does the pairing. */
167
+ displayLabel(semantics) {
168
+ return `${semantics.operationType} ${semantics.operationName ?? "<anonymous>"}`;
169
+ }
170
+ }
171
+ });
172
+ var GraphqlResolverSemanticsSchema = z3.object({
173
+ name: z3.literal("graphql-resolver"),
174
+ /**
175
+ * GraphQL type the resolver attaches to: "Query", "Mutation",
176
+ * "Subscription", or an object-type name. Null when the source never
177
+ * says which one, as with a `@Resolver()` class that has no argument.
178
+ */
179
+ typeName: z3.string().min(1).nullable(),
180
+ /** Field name on that type. */
181
+ fieldName: z3.string()
182
+ });
183
+ var graphqlResolverSemantics = defineBoundarySemantics({
184
+ name: "graphql-resolver",
185
+ schema: GraphqlResolverSemanticsSchema,
186
+ // The conventions describe the operation a client sent. They have no
187
+ // attribute for the resolver the server ran for one field of it.
188
+ semconv: {},
189
+ behavior: {
190
+ /** A resolver returns a field value, with no status. */
191
+ exchangesHttpResponses: false,
192
+ // A resolver runs because a client outside the process sent a query,
193
+ // even though the server calls it in its own process.
194
+ leavesTheProcess: true,
195
+ reportsUnpairedItself: false,
196
+ /**
197
+ * `"gql:<TypeName>.<fieldName>"`, or null when the type is null or
198
+ * the field is empty, which is how a resolver reports that the
199
+ * source never gave them.
200
+ */
201
+ identityKey(semantics) {
202
+ if (semantics.typeName === null || semantics.fieldName === "") {
203
+ return null;
204
+ }
205
+ return gqlIdentityKey(semantics.typeName, semantics.fieldName);
206
+ }
207
+ }
208
+ });
209
+ var IoSemanticsSchema = z4.object({
210
+ name: z4.literal("io"),
211
+ /** The stream, such as `stdout` or `stderr`. Null when the source does not say which. */
212
+ target: z4.string().nullable()
213
+ });
214
+ var ioSemantics = defineBoundarySemantics({
215
+ name: "io",
216
+ schema: IoSemanticsSchema,
217
+ // The semantic conventions have no attribute for a process's own streams.
218
+ semconv: {},
219
+ behavior: {
220
+ exchangesHttpResponses: false,
221
+ leavesTheProcess: true,
222
+ reportsUnpairedItself: false,
223
+ identityKey(semantics) {
224
+ return semantics.target === null ? null : ioIdentityKey(semantics.target);
225
+ },
226
+ // Nothing declares the reading side, so a write has nothing to meet.
227
+ canPair: () => false
228
+ }
229
+ });
230
+ function parseChannel(channel) {
231
+ const hash = channel.indexOf("#");
232
+ if (hash === -1) {
233
+ return { bus: null, subject: channel };
234
+ }
235
+ return { bus: channel.slice(0, hash), subject: channel.slice(hash + 1) };
236
+ }
237
+ function busesAgree(a, b) {
238
+ return a === null || b === null || a === b;
239
+ }
240
+ function channelsPair(a, b) {
241
+ const left = parseChannel(a);
242
+ const right = parseChannel(b);
243
+ return left.subject === right.subject && busesAgree(left.bus, right.bus);
244
+ }
245
+ var MessageBusSemanticsSchema = z5.object({
246
+ name: z5.literal("message-bus"),
247
+ messageBus: z5.enum([
248
+ // The conventions write these two AWS buses differently, one with an
249
+ // underscore and one with a dot. Both are as the conventions give them.
250
+ "aws_sqs",
251
+ "aws.sns",
252
+ "s3",
253
+ "eventbridge",
254
+ // A Kinesis stream and a Firehose delivery stream are different
255
+ // APIs with different destinations, so a stream named `orders` and
256
+ // a delivery stream named `orders` are not the same channel.
257
+ "aws_kinesis",
258
+ "aws_firehose",
259
+ "gcp_pubsub",
260
+ "bullmq",
261
+ "kafka",
262
+ "nats",
263
+ "cloudflare-queues",
264
+ "cloudflare-cron",
265
+ "cloudflare-tail"
266
+ ]),
267
+ /**
268
+ * A stable channel identifier: a CloudFormation logical id, a queue or
269
+ * topic name, a subject pattern, or `bus#detailType`. Null when this
270
+ * source does not say which channel, as with a send whose queue the
271
+ * code picks at run time, or a receive whose queue is set in the
272
+ * event-source mapping.
273
+ */
274
+ channel: z5.string().min(1).nullable()
275
+ });
276
+ var messageBusSemantics = defineBoundarySemantics({
277
+ name: "message-bus",
278
+ schema: MessageBusSemanticsSchema,
279
+ semconv: {
280
+ messageBus: { name: "messaging.system" },
281
+ channel: { name: "messaging.destination.name" }
282
+ },
283
+ behavior: {
284
+ /** A message goes onto the channel and nothing comes back. */
285
+ exchangesHttpResponses: false,
286
+ leavesTheProcess: true,
287
+ /**
288
+ * `checkMessageBus` reports every channel, unused ones included, so
289
+ * the generic unmatched list leaves them out.
290
+ */
291
+ reportsUnpairedItself: true,
292
+ /**
293
+ * A channel that is null here can still pair. The dedicated pass
294
+ * follows environment variable chains before comparing, so a
295
+ * consumer whose channel is set at run time meets the template that
296
+ * declares it.
297
+ */
298
+ canPair: () => true,
299
+ /**
300
+ * `"bus:<messageBus> <subject>"`, or null when the channel is null
301
+ * or its subject is empty.
302
+ *
303
+ * The key has the subject and leaves out the bus, so a template
304
+ * writing `default#order.placed` and a handler writing
305
+ * `order.placed` land in one bucket, where `sidesAgree` compares the
306
+ * buses. Keying on the bus would separate a side that cannot know
307
+ * its bus from the side that can.
308
+ *
309
+ * The bus technology stays in, because a queue and an event router
310
+ * are different destinations even with the same subject. Case stays
311
+ * too, because AWS compares detail-types and queue ids byte for byte.
312
+ */
313
+ identityKey(semantics) {
314
+ if (semantics.channel === null) {
315
+ return null;
316
+ }
317
+ const { subject } = parseChannel(semantics.channel);
318
+ if (subject === "") {
319
+ return null;
320
+ }
321
+ return busIdentityKey(semantics.messageBus, subject);
322
+ },
323
+ sidesAgree(a, b) {
324
+ if (a.channel === null || b.channel === null) {
325
+ return false;
326
+ }
327
+ return channelsPair(a.channel, b.channel);
328
+ },
329
+ /**
330
+ * The whole channel, bus included, where the identity key has only
331
+ * the subject. A reader looking at unmatched channels needs to see
332
+ * which bus each one was on.
333
+ */
334
+ displayLabel(semantics) {
335
+ if (semantics.channel === null) {
336
+ return `bus:${semantics.messageBus} (channel named at runtime)`;
337
+ }
338
+ return `bus:${semantics.messageBus} ${semantics.channel}`;
339
+ }
340
+ }
341
+ });
342
+ var MetricSemanticsSchema = z6.object({
343
+ name: z6.literal("metric"),
344
+ /** The system the series is stored in, such as `cloud-monitoring` or `cloudwatch`. */
345
+ metricSystem: z6.string(),
346
+ /**
347
+ * The type string the system uses for the metric, written the same
348
+ * way on both sides. Null when the reader could not work the string
349
+ * out, as with a query it could not parse. A null type pairs with
350
+ * nothing, so it cannot pair with an unrelated metric that happens to
351
+ * share its source text.
352
+ */
353
+ metricType: z6.string().nullable()
354
+ });
355
+ var metricSemantics = defineBoundarySemantics({
356
+ name: "metric",
357
+ schema: MetricSemanticsSchema,
358
+ // OpenTelemetry identifies a metric by its instrument name. It has no
359
+ // attribute for the metric's system or its type string.
360
+ semconv: {},
361
+ behavior: {
362
+ exchangesHttpResponses: false,
363
+ leavesTheProcess: true,
364
+ reportsUnpairedItself: false,
365
+ identityKey(semantics) {
366
+ if (semantics.metricType === null) {
367
+ return null;
368
+ }
369
+ return metricIdentityKey(semantics.metricSystem, semantics.metricType);
370
+ }
371
+ }
372
+ });
373
+ var HOLE_SPLIT = /(\{[^}]*\})/;
374
+ function isHole(piece) {
375
+ return piece.startsWith("{") && piece.endsWith("}");
376
+ }
377
+ function parseBoundaryName(name) {
378
+ const parts = name.split(HOLE_SPLIT).filter((piece) => piece !== "").map(
379
+ (piece) => isHole(piece) ? { type: "hole", label: piece.slice(1, -1) } : { type: "text", text: piece }
380
+ );
381
+ const only = parts.length === 1 ? parts[0] : void 0;
382
+ if (only !== void 0 && only.type === "hole") {
383
+ return { type: "reference", path: only.label.split(".") };
384
+ }
385
+ if (parts.some((part) => part.type === "hole")) {
386
+ return { type: "pattern", parts };
387
+ }
388
+ return { type: "literal", value: name };
389
+ }
390
+ function patternHole(label) {
391
+ return `{${label}}`;
392
+ }
393
+ function namesNothing(name) {
394
+ return parseBoundaryName(name).type === "reference";
395
+ }
396
+ function referenceOf(name) {
397
+ if (name.type !== "reference") {
398
+ return null;
399
+ }
400
+ const root = name.path[0];
401
+ if (root === void 0 || name.path.some((part) => part === "")) {
402
+ return null;
403
+ }
404
+ return { root, fields: name.path.slice(1) };
405
+ }
406
+ function referenceFromName(name) {
407
+ return referenceOf(parseBoundaryName(name));
408
+ }
409
+ var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
410
+ var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
411
+ function pathAfterOrigin(text) {
412
+ const withoutOrigin = text.replace(SCHEME_ORIGIN, "").replace(PROTOCOL_RELATIVE_ORIGIN, "");
413
+ const cut = withoutOrigin.search(/[?#]/);
414
+ return cut === -1 ? withoutOrigin : withoutOrigin.slice(0, cut);
415
+ }
416
+ var PRIVATE_SUFFIXES = [
417
+ "local",
418
+ "internal",
419
+ "localhost",
420
+ "lan",
421
+ "home.arpa",
422
+ "svc",
423
+ "test",
424
+ "example",
425
+ "invalid",
426
+ "example.com",
427
+ "example.net",
428
+ "example.org"
429
+ ];
430
+ var PRIVATE_IPV4 = /^(?:127\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.|0\.0\.0\.0$)/;
431
+ function hostnameOf(host) {
432
+ const lower = host.toLowerCase();
433
+ const name = lower.startsWith("[") ? lower.slice(0, lower.indexOf("]") + 1) : lower.split(":")[0] ?? lower;
434
+ return name.replace(/\.$/, "");
435
+ }
436
+ function hostCanBeOwn(host) {
437
+ const name = hostnameOf(host);
438
+ if (name === "[::1]" || /^\[f[cd]/.test(name) || PRIVATE_IPV4.test(name)) {
439
+ return true;
440
+ }
441
+ if (!name.includes(".")) {
442
+ return true;
443
+ }
444
+ return PRIVATE_SUFFIXES.some(
445
+ (suffix) => name === suffix || name.endsWith(`.${suffix}`)
446
+ );
447
+ }
448
+ var UNSPELLABLE_OPTION = /[()|?#{}]/;
449
+ function setPiece(options) {
450
+ if (options.some((option) => UNSPELLABLE_OPTION.test(option))) {
451
+ return null;
452
+ }
453
+ return `(${options.join("|")})`;
454
+ }
455
+ var ONE = { kind: "one" };
456
+ var OPTIONAL = { kind: "optional" };
457
+ var STAR = { kind: "star" };
458
+ var HOLE_SEGMENT = /^\{[^{}]*?([?+*]?)\}$/;
459
+ var SET_PIECE = /\(([^()]*)\)/;
460
+ var ALTERNATIVE_CAP = 64;
461
+ function alternativesOf(path6) {
462
+ let alternatives = [path6];
463
+ for (; ; ) {
464
+ const next = [];
465
+ let expanded = false;
466
+ for (const alternative of alternatives) {
467
+ const set = SET_PIECE.exec(alternative);
468
+ if (set === null || set.index === void 0) {
469
+ next.push(alternative);
470
+ continue;
471
+ }
472
+ expanded = true;
473
+ const head = alternative.slice(0, set.index);
474
+ const tail = alternative.slice(set.index + set[0].length);
475
+ for (const option of (set[1] ?? "").split("|")) {
476
+ next.push(head + option + tail);
477
+ }
478
+ }
479
+ if (!expanded) {
480
+ return alternatives;
481
+ }
482
+ if (next.length > ALTERNATIVE_CAP) {
483
+ return [path6.replace(new RegExp(SET_PIECE, "g"), "{value}")];
484
+ }
485
+ alternatives = next;
486
+ }
487
+ }
488
+ var HOLE_RANGE_ITEMS = {
489
+ "": [ONE],
490
+ "?": [OPTIONAL],
491
+ "+": [ONE, STAR],
492
+ "*": [STAR]
493
+ };
494
+ function escapedForRegex(text) {
495
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
496
+ }
497
+ function shapedItem(segment) {
498
+ const source = segment.split(/(\{[^}]*\}|\*)/g).map((part) => {
499
+ if (part === "*") {
500
+ return ".*";
501
+ }
502
+ if (part.startsWith("{")) {
503
+ return /[?*]\}$/.test(part) ? "[^/]*" : "[^/]+";
504
+ }
505
+ return escapedForRegex(part);
506
+ }).join("");
507
+ const holes = segment.match(/\{[^}]*\}|\*/g) ?? [];
508
+ const text = segment.replace(/\{[^}]*\}|\*/g, "");
509
+ return {
510
+ kind: "shaped",
511
+ shape: segment.replace(/\{[^}]*\}/g, "{}"),
512
+ test: new RegExp(`^${source}$`),
513
+ bare: holes.every((hole) => hole === "*" || /[?*]\}$/.test(hole)) ? text : null,
514
+ textLength: text.length
515
+ };
516
+ }
517
+ function itemsOf(segment) {
518
+ if (segment === "*") {
519
+ return [STAR];
520
+ }
521
+ const hole = HOLE_SEGMENT.exec(segment);
522
+ if (hole !== null) {
523
+ return HOLE_RANGE_ITEMS[hole[1] ?? ""] ?? [ONE];
524
+ }
525
+ if (segment.includes("{") || segment.includes("*")) {
526
+ return [shapedItem(segment)];
527
+ }
528
+ return [{ kind: "text", text: segment }];
529
+ }
530
+ function segmentsOf(alternative) {
531
+ const trimmed = alternative.startsWith("/") ? alternative.slice(1) : alternative;
532
+ return trimmed === "" ? [] : trimmed.split("/");
533
+ }
534
+ function patternSegmentsOf(alternative) {
535
+ const segments = segmentsOf(alternative);
536
+ const first = segments[0];
537
+ const opensWithBaseUrl = !alternative.startsWith("/") && first !== void 0 && HOLE_SEGMENT.test(first);
538
+ return opensWithBaseUrl ? segments.slice(1) : segments;
539
+ }
540
+ function pathAfterBaseUrl(path6) {
541
+ const segments = segmentsOf(path6);
542
+ const first = segments[0];
543
+ if (path6.startsWith("/") || first === void 0 || segments.length < 2 || !HOLE_SEGMENT.test(first)) {
544
+ return path6;
545
+ }
546
+ return `/${segments.slice(1).join("/")}`;
547
+ }
548
+ var PATTERNS = /* @__PURE__ */ new Map();
549
+ var PATTERN_CACHE_LIMIT = 5e4;
550
+ function patternOf(path6) {
551
+ const known = PATTERNS.get(path6);
552
+ if (known !== void 0) {
553
+ return known;
554
+ }
555
+ const pattern = alternativesOf(path6).map(
556
+ (alternative) => patternSegmentsOf(alternative).flatMap(itemsOf)
557
+ );
558
+ if (PATTERNS.size >= PATTERN_CACHE_LIMIT) {
559
+ PATTERNS.clear();
560
+ }
561
+ PATTERNS.set(path6, pattern);
562
+ return pattern;
563
+ }
564
+ function literalItemsOf(path6) {
565
+ return segmentsOf(path6).map((text) => ({ kind: "text", text }));
566
+ }
567
+ function absorbsAnything(item) {
568
+ return item.kind === "one" || item.kind === "optional" || item.kind === "star";
569
+ }
570
+ function meetOnOneSegment(a, b) {
571
+ if (absorbsAnything(a) || absorbsAnything(b)) {
572
+ return true;
573
+ }
574
+ if (a.kind === "text" && b.kind === "text") {
575
+ return a.text === b.text;
576
+ }
577
+ if (a.kind === "text" && b.kind === "shaped") {
578
+ return b.test.test(a.text);
579
+ }
580
+ if (a.kind === "shaped" && b.kind === "text") {
581
+ return a.test.test(b.text);
582
+ }
583
+ return a.kind === "shaped" && b.kind === "shaped" && a.shape === b.shape;
584
+ }
585
+ function skippable(item) {
586
+ return item.kind === "optional" || item.kind === "star";
587
+ }
588
+ function itemsMeet(a, b) {
589
+ const width = b.length + 1;
590
+ const seen = new Uint8Array((a.length + 1) * width);
591
+ const queue = [0];
592
+ seen[0] = 1;
593
+ const visit = (i, j) => {
594
+ const at = i * width + j;
595
+ if (seen[at] === 0) {
596
+ seen[at] = 1;
597
+ queue.push(at);
598
+ }
599
+ };
600
+ while (queue.length > 0) {
601
+ const at = queue.pop();
602
+ const i = Math.floor(at / width);
603
+ const j = at % width;
604
+ if (i === a.length && j === b.length) {
605
+ return true;
606
+ }
607
+ const left = a[i];
608
+ const right = b[j];
609
+ if (left !== void 0 && skippable(left)) {
610
+ visit(i + 1, j);
611
+ }
612
+ if (right !== void 0 && skippable(right)) {
613
+ visit(i, j + 1);
614
+ }
615
+ if (left !== void 0 && right !== void 0) {
616
+ if (meetOnOneSegment(left, right)) {
617
+ visit(
618
+ left.kind === "star" ? i : i + 1,
619
+ right.kind === "star" ? j : j + 1
620
+ );
621
+ }
622
+ }
623
+ }
624
+ return false;
625
+ }
626
+ function pathsMeet(a, b) {
627
+ const left = patternOf(a);
628
+ const right = patternOf(b);
629
+ return left.some((one) => right.some((other) => itemsMeet(one, other)));
630
+ }
631
+ function patternAdmits(declared, request) {
632
+ const literal = literalItemsOf(request);
633
+ return patternOf(declared).some((items) => itemsMeet(items, literal));
634
+ }
635
+ function pathSpansShapes(path6) {
636
+ return /\{[^{}]*[?+*]\}|\(|(?:^|\/)\*(?:\/|$)/.test(path6);
637
+ }
638
+ function countOf(items, kinds) {
639
+ return items.filter((item) => kinds.includes(item.kind)).length;
640
+ }
641
+ function readingSpecificity(items) {
642
+ return [
643
+ countOf(items, ["text"]),
644
+ countOf(items, ["shaped"]),
645
+ -countOf(items, ["optional", "star"])
646
+ ];
647
+ }
648
+ function segmentFit(route, request) {
649
+ if (!meetOnOneSegment(route, request)) {
650
+ return "apart";
651
+ }
652
+ if (request.kind === "text") {
653
+ if (route.kind === "text") {
654
+ return "stated";
655
+ }
656
+ return route.kind === "shaped" ? "shaped" : "free";
657
+ }
658
+ if (absorbsAnything(route)) {
659
+ return "free";
660
+ }
661
+ if (request.kind !== "shaped") {
662
+ return "guess";
663
+ }
664
+ if (route.kind === "shaped") {
665
+ return "free";
666
+ }
667
+ return route.kind === "text" && request.bare === route.text ? "stated" : "guess";
668
+ }
669
+ function shapedText(item) {
670
+ return item.kind === "shaped" ? item.textLength : 0;
671
+ }
672
+ function betterFit(a, b) {
673
+ return a === void 0 || compareRanks(b, a) > 0;
674
+ }
675
+ function bestFit(route, request) {
676
+ const width = request.length + 1;
677
+ const best = new Array((route.length + 1) * width);
678
+ best[0] = [0, 0];
679
+ const offer = (i, j, fit) => {
680
+ const at = i * width + j;
681
+ if (betterFit(best[at], fit)) {
682
+ best[at] = fit;
683
+ }
684
+ };
685
+ for (let reached = 0; reached <= route.length + request.length; reached += 1) {
686
+ for (let i = Math.max(0, reached - request.length); i <= Math.min(route.length, reached); i += 1) {
687
+ const j = reached - i;
688
+ const fit = best[i * width + j];
689
+ if (fit === void 0) {
690
+ continue;
691
+ }
692
+ const left = route[i];
693
+ const right = request[j];
694
+ if (left !== void 0 && skippable(left)) {
695
+ offer(i + 1, j, fit);
696
+ }
697
+ if (right !== void 0 && skippable(right)) {
698
+ offer(i, j + 1, fit);
699
+ }
700
+ if (left === void 0 || right === void 0) {
701
+ continue;
702
+ }
703
+ if (left.kind === "star" && right.kind === "star") {
704
+ continue;
705
+ }
706
+ const kind = segmentFit(left, right);
707
+ if (kind === "apart" || kind === "guess") {
708
+ continue;
709
+ }
710
+ offer(
711
+ left.kind === "star" ? i : i + 1,
712
+ right.kind === "star" ? j : j + 1,
713
+ [
714
+ fit[0] + (kind === "stated" ? 1 : 0),
715
+ fit[1] + (kind === "shaped" ? shapedText(left) : 0)
716
+ ]
717
+ );
718
+ }
719
+ }
720
+ return best[route.length * width + request.length] ?? null;
721
+ }
722
+ function requestRank(route, request) {
723
+ let found = null;
724
+ for (const routeItems of patternOf(route)) {
725
+ for (const requestItems of patternOf(request)) {
726
+ const fit = bestFit(routeItems, requestItems);
727
+ const rank = fit === null ? null : [...fit, ...readingSpecificity(routeItems)];
728
+ if (rank !== null && (found === null || compareRanks(rank, found) > 0)) {
729
+ found = rank;
730
+ }
731
+ }
732
+ }
733
+ return found;
734
+ }
735
+ function isCatchAll(route) {
736
+ return patternOf(route).some(
737
+ (items) => items.length > 0 && items.every(absorbsAnything) && items.some((item) => item.kind === "star")
738
+ );
739
+ }
740
+ function compareRanks(a, b) {
741
+ const length = Math.max(a.length, b.length);
742
+ for (let index = 0; index < length; index += 1) {
743
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
744
+ if (difference !== 0) {
745
+ return difference;
746
+ }
747
+ }
748
+ return 0;
749
+ }
750
+ var RestSemanticsSchema = z7.object({
751
+ name: z7.literal("rest"),
752
+ /**
753
+ * Uppercase HTTP method ("GET", "POST", …), `"*"` for a handler that
754
+ * responds to every method, or null when this source does not say
755
+ * which method.
756
+ */
757
+ method: z7.string().min(1).nullable(),
758
+ /** Normalized route path ("/users/{id}"), or null when this source does not give one. */
759
+ path: z7.string().min(1).nullable(),
760
+ /**
761
+ * Status codes the producing source declared explicitly, such as
762
+ * OpenAPI responses or CloudFormation MethodResponses. They are kept
763
+ * here so the pairing layer can see them without unwrapping any
764
+ * metadata. Absent or empty for an inferred source.
765
+ */
766
+ declaredResponses: z7.array(z7.number()).optional(),
767
+ /**
768
+ * The host a client wrote in an absolute URL, with its port
769
+ * (`api.example.com:8443`). A piece suss could not read is written
770
+ * `{name}`. Absent for a relative URL and for a route.
771
+ */
772
+ host: z7.string().min(1).optional(),
773
+ /**
774
+ * True for a route the framework also serves with `.{format}` after
775
+ * its path, the way Rails serves `/search.json` from `get "search"`.
776
+ */
777
+ optionalFormat: z7.boolean().optional()
778
+ });
779
+ var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
780
+ function baseUrlReference(semantics) {
781
+ const label = OPENING_HOLE.exec(semantics.path ?? "")?.[1];
782
+ return label === void 0 ? null : referenceFromName(patternHole(label));
783
+ }
784
+ function normalizePath(path6) {
785
+ let normalized = withoutParameterConstraints(path6).replace(
786
+ /:([a-zA-Z_]\w*)([?+*]?)/g,
787
+ "{$1$2}"
788
+ );
789
+ if (normalized.length > 1 && normalized.endsWith("/")) {
790
+ normalized = normalized.slice(0, -1);
791
+ }
792
+ normalized = normalized.replace(
793
+ /\{[^}]+\}|[^{]+/g,
794
+ (segment) => segment.startsWith("{") ? segment : segment.toLowerCase()
795
+ );
796
+ return normalized;
797
+ }
798
+ function withoutParameterConstraints(path6) {
799
+ let out = "";
800
+ let i = 0;
801
+ while (i < path6.length) {
802
+ const name = /^:[a-zA-Z_]\w*\(/.exec(path6.slice(i));
803
+ if (name === null) {
804
+ out += path6[i];
805
+ i += 1;
806
+ continue;
807
+ }
808
+ const open = i + name[0].length - 1;
809
+ const close = closingParenthesis(path6, open);
810
+ if (close === -1) {
811
+ return out + path6.slice(i);
812
+ }
813
+ out += path6.slice(i, open);
814
+ i = close + 1;
815
+ }
816
+ return out;
817
+ }
818
+ function closingParenthesis(text, open) {
819
+ let depth = 0;
820
+ for (let i = open; i < text.length; i += 1) {
821
+ const char = text[i];
822
+ if (char === "\\") {
823
+ i += 1;
824
+ continue;
825
+ }
826
+ if (char === "(") {
827
+ depth += 1;
828
+ }
829
+ if (char === ")") {
830
+ depth -= 1;
831
+ if (depth === 0) {
832
+ return i;
833
+ }
834
+ }
835
+ }
836
+ return -1;
837
+ }
838
+ function pathShape(path6) {
839
+ return normalizePath(path6).replace(/\{[^{}]*?([?+*]?)\}/g, "{$1}");
840
+ }
841
+ function methodsAgree(a, b) {
842
+ if (a === null || b === null) {
843
+ return false;
844
+ }
845
+ return a === b || a === "*" || b === "*";
846
+ }
847
+ function routePathAdmits(declaredPath, requestPath) {
848
+ return patternAdmits(normalizePath(declaredPath), normalizePath(requestPath));
849
+ }
850
+ function readHost(semantics) {
851
+ const host = semantics.host;
852
+ return host === void 0 || /[{(]/.test(host) ? void 0 : host;
853
+ }
854
+ function hostsAgree(a, b) {
855
+ return readHost(a) === readHost(b);
856
+ }
857
+ function hostUnread(semantics) {
858
+ if (semantics.host !== void 0) {
859
+ return readHost(semantics) === void 0;
860
+ }
861
+ return baseUrlReference(semantics) !== null;
862
+ }
863
+ function servedPath(semantics, path6) {
864
+ const normalized = normalizePath(path6);
865
+ return semantics.optionalFormat === true ? `${normalized}(|.{format})` : normalized;
866
+ }
867
+ function withoutHost(semantics) {
868
+ const { host: _host, ...rest } = semantics;
869
+ return rest;
870
+ }
871
+ function groundedBaseUrl(semantics, deployment) {
872
+ const reference = baseUrlReference(semantics);
873
+ if (reference === null) {
874
+ return null;
875
+ }
876
+ const base = deployment.setTo(reference);
877
+ if (base === null) {
878
+ return null;
879
+ }
880
+ const rest = OPENING_HOLE.exec(semantics.path ?? "")?.[2] ?? "";
881
+ const grounded = pathAfterOrigin(`${base}${rest}`);
882
+ return { ...semantics, path: grounded === "" ? "/" : grounded };
883
+ }
884
+ var restSemantics = defineBoundarySemantics({
885
+ name: "rest",
886
+ schema: RestSemanticsSchema,
887
+ semconv: {
888
+ // A span states the one method a request used, so a route that
889
+ // responds to every method has nothing to compare.
890
+ method: { name: "http.request.method", placeholderValues: ["*"] },
891
+ path: { name: "http.route" }
892
+ // declaredResponses is a list, and http.response.status_code is
893
+ // the one status a request got back.
894
+ },
895
+ behavior: {
896
+ /** A request goes out, a status and a body come back. */
897
+ exchangesHttpResponses: true,
898
+ leavesTheProcess: true,
899
+ reportsUnpairedItself: false,
900
+ /** `"METHOD /normalized/path"`, or null when either half is missing. */
901
+ identityKey(semantics) {
902
+ if (semantics.method === null || semantics.path === null) {
903
+ return null;
904
+ }
905
+ return `${semantics.method.toUpperCase()} ${normalizePath(semantics.path)}`;
906
+ },
907
+ /**
908
+ * The bucket has only the path, so a `"*"` route lands with the
909
+ * consumers that each give a method, and `methodsAgree` compares the
910
+ * methods inside the bucket. The identity key is still what a reader
911
+ * sees and what a suppression targets ("GET /users", "* /users").
912
+ *
913
+ * Parameter names are dropped too. A hand-written spec writes
914
+ * `{userId}` where the Express route that serves it writes `:id`,
915
+ * and both match the same requests. Pairing on the name would leave
916
+ * two boundaries that never meet.
917
+ */
918
+ pairingKey(semantics) {
919
+ if (semantics.method === null || semantics.path === null) {
920
+ return null;
921
+ }
922
+ const shape = pathShape(pathAfterBaseUrl(normalizePath(semantics.path)));
923
+ const host = readHost(semantics);
924
+ return host === void 0 ? `rest ${shape}` : `rest //${host}${shape}`;
925
+ },
926
+ /**
927
+ * A spec writes `/teams/{team_id}` where the route that serves it
928
+ * writes `/teams/:id`. The method stays in, so a `"*"` handler is
929
+ * never compared with the document for one method.
930
+ */
931
+ operationKey(semantics) {
932
+ if (semantics.method === null || semantics.path === null) {
933
+ return null;
934
+ }
935
+ return `${semantics.method.toUpperCase()} ${pathShape(semantics.path)}`;
936
+ },
937
+ sidesAgree(a, b) {
938
+ return methodsAgree(a.method, b.method) && hostsAgree(a, b);
939
+ },
940
+ spansBuckets(semantics) {
941
+ return semantics.path !== null && (semantics.optionalFormat === true || pathSpansShapes(normalizePath(semantics.path)));
942
+ },
943
+ bucketsMeet(a, b) {
944
+ return a.path !== null && b.path !== null && hostsAgree(a, b) && pathsMeet(servedPath(a, a.path), servedPath(b, b.path));
945
+ },
946
+ /**
947
+ * A catch-all route never takes a call whose host suss could not
948
+ * read, since nothing else ties that call to this app.
949
+ */
950
+ pairRank(provider, consumer) {
951
+ if (provider.path === null || consumer.path === null) {
952
+ return [];
953
+ }
954
+ const route = normalizePath(provider.path);
955
+ if (hostUnread(consumer) && isCatchAll(route)) {
956
+ return null;
957
+ }
958
+ return requestRank(
959
+ servedPath(provider, provider.path),
960
+ normalizePath(consumer.path)
961
+ );
962
+ },
963
+ /**
964
+ * The identity key, with a missing half still readable: `ANY` when
965
+ * the source gave no method, and `?` when it gave no path. Null when
966
+ * both are missing, since then there is nothing to show.
967
+ */
968
+ displayLabel(semantics) {
969
+ if (semantics.method === null && semantics.path === null) {
970
+ return null;
971
+ }
972
+ const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
973
+ const path6 = semantics.path === null ? "?" : normalizePath(semantics.path);
974
+ return `${method} ${path6}`;
975
+ },
976
+ /**
977
+ * With both halves present, the method goes through `methodsAgree`
978
+ * (so a `"*"` route matches every method) and the path through
979
+ * `routePathAdmits`. A route missing either half might still handle
980
+ * the request, and this declaration cannot settle that, so the
981
+ * result is `"unknown"`.
982
+ */
983
+ servesRequest(semantics, method, path6) {
984
+ if (semantics.method === null || semantics.path === null) {
985
+ return "unknown";
986
+ }
987
+ if (!methodsAgree(semantics.method, method.toUpperCase())) {
988
+ return "nomatch";
989
+ }
990
+ return routePathAdmits(semantics.path, path6) ? "match" : "nomatch";
991
+ },
992
+ /**
993
+ * A route the registration pattern admits is inside it. A route
994
+ * without a path counts as outside, because nothing shows that the
995
+ * pattern covers it.
996
+ */
997
+ withinScope(semantics, scope) {
998
+ return semantics.path !== null && routePathAdmits(scope, semantics.path);
999
+ },
1000
+ /**
1001
+ * A call whose base URL the deployment fills in, resolved to the
1002
+ * path it reaches, and a call to a host that can be the project's
1003
+ * own, with the host left out so it meets the route.
1004
+ *
1005
+ * The source cannot settle a base URL on its own, so the adapter
1006
+ * leaves the hole in. `API_BASE` could be `http://backend.internal`,
1007
+ * and then the path is `/orders`. It could equally be `/api/v2`, and
1008
+ * then the path is `/api/v2/orders`. Filling in the deployed value
1009
+ * and reading the path back out gets the right one either way, and
1010
+ * a base the deployment sets counts as the app's own.
1011
+ */
1012
+ /** A listed host matches with or without the port the call gives. */
1013
+ withoutOwnHost(semantics, ownHosts) {
1014
+ const host = semantics.host?.toLowerCase();
1015
+ if (host === void 0) {
1016
+ return null;
1017
+ }
1018
+ return ownHosts.has(host) || ownHosts.has(host.replace(/:\d+$/, "")) ? withoutHost(semantics) : null;
1019
+ },
1020
+ groundName(semantics, deployment) {
1021
+ const based = groundedBaseUrl(semantics, deployment);
1022
+ const grounded = based ?? semantics;
1023
+ const host = readHost(grounded);
1024
+ return host !== void 0 && hostCanBeOwn(host) ? withoutHost(grounded) : based;
1025
+ },
1026
+ nameReference: baseUrlReference,
1027
+ ruleBoundary: {
1028
+ // "METHOD /path": one token, a space, then an absolute path.
1029
+ claims(raw) {
1030
+ return /^\S+ +\//.test(raw.trim());
1031
+ },
1032
+ normalize(raw) {
1033
+ const trimmed = raw.trim();
1034
+ const spaceIdx = trimmed.indexOf(" ");
1035
+ const method = trimmed.slice(0, spaceIdx).toUpperCase();
1036
+ return `${method} ${normalizePath(trimmed.slice(spaceIdx + 1).trim())}`;
1037
+ }
1038
+ }
1039
+ }
1040
+ });
1041
+ var DeployableUnitSchema = z8.object({
1042
+ deploymentTarget: z8.enum([
1043
+ "lambda",
1044
+ "ecs-task",
1045
+ "container",
1046
+ "k8s-deployment",
1047
+ "worker"
1048
+ ]),
1049
+ // An empty name would agree with every other empty name, so a unit
1050
+ // with no name has to leave the field off instead.
1051
+ instanceName: z8.string().min(1)
1052
+ });
1053
+ var RuntimeConfigSemanticsSchema = DeployableUnitSchema.partial({
1054
+ deploymentTarget: true,
1055
+ instanceName: true
1056
+ }).extend({
1057
+ name: z9.literal("runtime-config")
1058
+ });
1059
+ var runtimeConfigSemantics = defineBoundarySemantics({
1060
+ name: "runtime-config",
1061
+ schema: RuntimeConfigSemanticsSchema,
1062
+ // service.name and cloud.resource_id identify the same deployable, but
1063
+ // instanceName is the template's logical id, a different string from
1064
+ // both.
1065
+ semconv: {},
1066
+ behavior: {
1067
+ /** A process reads its config at startup, and nothing replies. */
1068
+ exchangesHttpResponses: false,
1069
+ // Whoever deploys the process puts the value there, so the value
1070
+ // comes from outside the code.
1071
+ leavesTheProcess: true,
1072
+ reportsUnpairedItself: false,
1073
+ identityKey: () => null,
1074
+ // A reader sees which deployable's environment this is. Code that
1075
+ // reads a variable does not say which deployable runs it, so a read
1076
+ // has no name.
1077
+ displayLabel(semantics) {
1078
+ const instance = semantics.instanceName;
1079
+ return instance === void 0 ? null : `runtime-config:${instance}`;
1080
+ }
1081
+ }
1082
+ });
1083
+ var StorageSemanticsSchema = z10.object({
1084
+ name: z10.literal("storage"),
1085
+ /**
1086
+ * The storage system: an OpenTelemetry value such as `postgresql` or
1087
+ * `aws.dynamodb`, or suss's own word for one the conventions do not
1088
+ * cover, such as `s3`. Null when the reader could not work out which
1089
+ * system the source uses. Two products' containers can share a name,
1090
+ * and this field keeps them apart. The pairing pass decides how to
1091
+ * treat a null.
1092
+ */
1093
+ storageSystem: z10.string().nullable(),
1094
+ /**
1095
+ * The ORM, schema, or deployment scope. A single-database setup uses
1096
+ * `"default"`, and a monorepo with several schemas gives each one its
1097
+ * own value.
1098
+ */
1099
+ scope: z10.string(),
1100
+ /**
1101
+ * The table, bucket, collection or index, as the source declares it.
1102
+ * Null when the reader could not work out the container, and a null
1103
+ * container pairs with nothing, so it cannot match by accident of
1104
+ * shared source text. The string uses the boundary-name syntax (a
1105
+ * literal, a pattern with `{}` holes, or a reference), and only
1106
+ * `parseBoundaryName` should read it.
1107
+ */
1108
+ container: z10.string().nullable(),
1109
+ /**
1110
+ * A secondary way into the container, such as a DynamoDB global
1111
+ * secondary index or an Elasticsearch alias, each with its own key
1112
+ * fields. Null means the container's primary key. A query through an
1113
+ * index and a query through the table are different accesses, so
1114
+ * they pair separately.
1115
+ */
1116
+ accessPath: z10.string().nullable()
1117
+ });
1118
+ function containerReference(semantics) {
1119
+ return semantics.container === null ? null : referenceFromName(semantics.container);
1120
+ }
1121
+ var storageSemantics = defineBoundarySemantics({
1122
+ name: "storage",
1123
+ schema: StorageSemanticsSchema,
1124
+ semconv: {
1125
+ storageSystem: { name: "db.system.name" },
1126
+ // suss writes "default" when the source did not say which database.
1127
+ scope: { name: "db.namespace", placeholderValues: ["default"] },
1128
+ container: { name: "db.collection.name" }
1129
+ // The conventions have no attribute for a secondary index, so
1130
+ // accessPath is left out.
1131
+ },
1132
+ behavior: {
1133
+ /** A query returns rows or items, with no status. */
1134
+ exchangesHttpResponses: false,
1135
+ leavesTheProcess: true,
1136
+ reportsUnpairedItself: false,
1137
+ identityKey: () => null,
1138
+ displayLabel: storageLabel,
1139
+ /**
1140
+ * A container the source gives as a variable is whatever the
1141
+ * deployment sets that variable to. `{SUBSCRIBERS_TABLE}` in the
1142
+ * code and `prod-subscribers-v1` in the manifest are one table.
1143
+ */
1144
+ groundName(semantics, deployment) {
1145
+ const reference = containerReference(semantics);
1146
+ if (reference === null) {
1147
+ return null;
1148
+ }
1149
+ const name = deployment.setTo(reference);
1150
+ return name === null ? null : { ...semantics, container: name };
1151
+ },
1152
+ nameReference: containerReference
1153
+ }
1154
+ });
1155
+ function storageLabel(semantics) {
1156
+ return `${storageSystemLabel(semantics)}:${storageContainerLabel(semantics)}`;
1157
+ }
1158
+ function storageSystemLabel(semantics) {
1159
+ return semantics.storageSystem ?? UNKNOWN_ENGINE;
1160
+ }
1161
+ var UNKNOWN_ENGINE = "<unknown engine>";
1162
+ function storageContainerLabel(semantics) {
1163
+ const container = semantics.container ?? "<unnamed container>";
1164
+ const addressed = semantics.accessPath === null ? container : `${container}#${semantics.accessPath}`;
1165
+ return semantics.scope === "default" ? addressed : `${semantics.scope}/${addressed}`;
1166
+ }
1167
+ var UnitInvocationSemanticsSchema = DeployableUnitSchema.extend({
1168
+ name: z11.literal("unit-invocation"),
1169
+ instanceName: DeployableUnitSchema.shape.instanceName.nullable()
1170
+ });
1171
+ function calleeReference(semantics) {
1172
+ return semantics.instanceName === null ? null : referenceFromName(semantics.instanceName);
1173
+ }
1174
+ var unitInvocationSemantics = defineBoundarySemantics({
1175
+ name: "unit-invocation",
1176
+ schema: UnitInvocationSemanticsSchema,
1177
+ // faas.name is the deployed function's own name and instanceName is
1178
+ // the deployment template's logical id, which is a different string.
1179
+ // Runtime config leaves the pair off for the same reason.
1180
+ semconv: {},
1181
+ behavior: {
1182
+ /**
1183
+ * A synchronous invoke gives back whatever the handler returned,
1184
+ * which is a value rather than a status and a body. A handler's
1185
+ * return only becomes an HTTP response under a proxy integration,
1186
+ * and that route gets a REST binding instead of this one.
1187
+ */
1188
+ exchangesHttpResponses: false,
1189
+ leavesTheProcess: true,
1190
+ /**
1191
+ * A unit nothing in the run invokes is the ordinary case, since
1192
+ * most services are reached from a stack suss never read. So an
1193
+ * uninvoked unit stays in the generic unmatched list rather than
1194
+ * being reported as a problem here.
1195
+ */
1196
+ reportsUnpairedItself: false,
1197
+ /**
1198
+ * A callee the source gives only as a variable has no name until the
1199
+ * deployment fills it in. It gets no key, since a key built from the
1200
+ * variable would pair only with itself.
1201
+ */
1202
+ identityKey(semantics) {
1203
+ if (semantics.instanceName === null || namesNothing(semantics.instanceName)) {
1204
+ return null;
1205
+ }
1206
+ return unitIdentityKey(
1207
+ semantics.deploymentTarget,
1208
+ semantics.instanceName
1209
+ );
1210
+ },
1211
+ displayLabel(semantics) {
1212
+ if (semantics.instanceName === null) {
1213
+ return `unit:${semantics.deploymentTarget} (named at runtime)`;
1214
+ }
1215
+ return unitIdentityKey(
1216
+ semantics.deploymentTarget,
1217
+ semantics.instanceName
1218
+ );
1219
+ },
1220
+ /**
1221
+ * A call that reads its callee's name from the environment reaches
1222
+ * whichever resource the template wires that variable to. The name
1223
+ * is grounded to the logical id and not the deployed name, because
1224
+ * the invoked unit's own summary is keyed by the logical id.
1225
+ */
1226
+ groundName(semantics, deployment) {
1227
+ const reference = calleeReference(semantics);
1228
+ if (reference === null) {
1229
+ return null;
1230
+ }
1231
+ const logicalId = deployment.pointsAt(reference);
1232
+ return logicalId === null ? null : { ...semantics, instanceName: logicalId };
1233
+ },
1234
+ nameReference: calleeReference
1235
+ }
1236
+ });
1237
+ var SemanticsSchema = z12.discriminatedUnion("name", [
1238
+ restSemantics.schema,
1239
+ functionCallSemantics.schema,
1240
+ graphqlResolverSemantics.schema,
1241
+ graphqlOperationSemantics.schema,
1242
+ runtimeConfigSemantics.schema,
1243
+ storageSemantics.schema,
1244
+ messageBusSemantics.schema,
1245
+ metricSemantics.schema,
1246
+ unitInvocationSemantics.schema,
1247
+ ioSemantics.schema
1248
+ ]);
1249
+ var DEFINITIONS = [
1250
+ restSemantics,
1251
+ functionCallSemantics,
1252
+ graphqlResolverSemantics,
1253
+ graphqlOperationSemantics,
1254
+ runtimeConfigSemantics,
1255
+ storageSemantics,
1256
+ messageBusSemantics,
1257
+ metricSemantics,
1258
+ unitInvocationSemantics,
1259
+ ioSemantics
1260
+ ];
1261
+ var BY_NAME = new Map(
1262
+ DEFINITIONS.map((d) => [d.name, d])
1263
+ );
1264
+ var ConfidenceSourceSchema = z13.enum([
1265
+ "inferred_static",
1266
+ "inferred_ai",
1267
+ "declared",
1268
+ "derived"
1269
+ ]);
1270
+ var ConfidenceLevelSchema = z13.enum(["high", "medium", "low"]);
1271
+ var CorroborationSchema = z13.object({
1272
+ outcome: z13.enum(["observed", "refuted", "untested"]),
1273
+ /** Executions that produced a verdict for this claim. */
1274
+ runs: z13.number(),
1275
+ /** Present when refuted: the input and observation that disagreed. */
1276
+ counterexample: z13.unknown().optional(),
1277
+ /** Present when untested: why no verdict was reachable. */
1278
+ reason: z13.string().optional()
1279
+ });
1280
+ var ConfidenceInfoSchema = z13.object({
1281
+ source: ConfidenceSourceSchema,
1282
+ level: ConfidenceLevelSchema,
1283
+ corroboration: CorroborationSchema.optional()
1284
+ });
1285
+ var SourceLocationSchema = z13.object({
1286
+ file: z13.string(),
1287
+ /** Line numbers, for a person reading the summary or an editor link. */
1288
+ range: z13.object({ start: z13.number(), end: z13.number() }),
1289
+ /**
1290
+ * Character offsets of the unit in its file. Identity and joins use
1291
+ * these, because two functions can share a line but never an offset
1292
+ * range. `range` gives the same place in lines. Absent on a summary
1293
+ * with no source position, such as one read from a contract.
1294
+ */
1295
+ span: z13.object({ start: z13.number(), end: z13.number() }).optional(),
1296
+ exportName: z13.string().nullable(),
1297
+ /**
1298
+ * The project the extract ran on, by the name the project gives
1299
+ * itself.
1300
+ *
1301
+ * Paths are relative to wherever the extract ran, so two services in
1302
+ * one repository can both report `src/handlers.ts`, and merging their
1303
+ * summaries would mix them up. This field tells them apart, and a
1304
+ * reader groups by it.
1305
+ */
1306
+ workspace: z13.string().optional(),
1307
+ /**
1308
+ * The module of the application the unit belongs to, by the name the
1309
+ * project's `suss.json` gives it. Set only when the project lists its
1310
+ * modules, and then on every unit whose file is under a module's root.
1311
+ */
1312
+ module: z13.string().optional()
1313
+ });
1314
+ var BoundaryBindingSchema = z13.object({
1315
+ transport: z13.string(),
1316
+ semantics: SemanticsSchema,
1317
+ recognition: z13.string()
1318
+ });
1319
+ var TypeShapeSchema = z13.lazy(
1320
+ () => z13.discriminatedUnion("type", [
1321
+ z13.object({
1322
+ type: z13.literal("record"),
1323
+ properties: z13.record(z13.string(), TypeShapeSchema),
1324
+ spreads: z13.array(z13.object({ sourceText: z13.string() })).optional()
1325
+ }),
1326
+ z13.object({ type: z13.literal("dictionary"), values: TypeShapeSchema }),
1327
+ z13.object({ type: z13.literal("array"), items: TypeShapeSchema }),
1328
+ z13.object({
1329
+ type: z13.literal("literal"),
1330
+ value: z13.union([z13.string(), z13.number(), z13.boolean()]),
1331
+ raw: z13.string().optional()
1332
+ }),
1333
+ z13.object({ type: z13.literal("text") }),
1334
+ z13.object({ type: z13.literal("integer") }),
1335
+ z13.object({ type: z13.literal("number") }),
1336
+ z13.object({ type: z13.literal("boolean") }),
1337
+ z13.object({ type: z13.literal("null") }),
1338
+ z13.object({ type: z13.literal("undefined") }),
1339
+ z13.object({
1340
+ type: z13.literal("union"),
1341
+ variants: z13.array(TypeShapeSchema)
1342
+ }),
1343
+ z13.object({
1344
+ type: z13.literal("ref"),
1345
+ name: z13.string(),
1346
+ from: z13.string().optional(),
1347
+ def: z13.string().optional()
1348
+ }),
1349
+ z13.object({ type: z13.literal("unknown") })
1350
+ ])
1351
+ );
1352
+
1353
+ // ../ir-core/dist/index.js
1354
+ import { z as z14 } from "zod";
1355
+ import { z as z22 } from "zod";
1356
+ var RelationSchema = z14.enum([
1357
+ "provides",
1358
+ "reads",
1359
+ "writes",
1360
+ "invokes"
1361
+ ]);
1362
+ var EffectRelationSchema = RelationSchema.exclude(["provides"]);
1363
+ var SuppressionSideSchema = z22.object({
1364
+ summary: z22.string().optional(),
1365
+ transitionId: z22.string().optional()
1366
+ }).optional();
1367
+ var SuppressionRuleSchema = z22.object({
1368
+ /**
1369
+ * The finding kind to match, behavioral or intent. The schema accepts
1370
+ * any string because ir-core cannot import either IR's kind enum. The
1371
+ * CLI's loader checks it against both.
1372
+ */
1373
+ kind: z22.string().optional(),
1374
+ /**
1375
+ * The boundary's human-readable key, such as "GET /pet/{petId}" or
1376
+ * "fn:@acme/api::getUser". A REST key goes through the same path
1377
+ * normalizer the checkers use, so `:id` and `{id}` compare equal.
1378
+ */
1379
+ boundary: z22.string().optional(),
1380
+ /**
1381
+ * Consumer-side discriminators, the narrowest useful match. They
1382
+ * apply only to behavioral findings. A rule that sets `consumer`
1383
+ * never matches an intent finding, because an intent finding has no
1384
+ * consumer side.
1385
+ */
1386
+ consumer: SuppressionSideSchema,
1387
+ /**
1388
+ * Provider-side discriminators, matching the same fields as
1389
+ * `consumer`. A finding about a status the provider produces has its
1390
+ * transition id on this side, and that id is the only value narrow
1391
+ * enough to pick out that one finding. As with `consumer`, a rule
1392
+ * that sets `provider` never matches an intent finding.
1393
+ */
1394
+ provider: SuppressionSideSchema,
1395
+ /**
1396
+ * The title of one PRD scenario. A PRD finding's boundary is the
1397
+ * whole PRD, so without this a rule accepts every scenario in it,
1398
+ * including one added later. A rule that sets `scenario` matches
1399
+ * only intent findings about a scenario with that title.
1400
+ */
1401
+ scenario: z22.string().optional(),
1402
+ /**
1403
+ * "narrow", the default, requires `kind` plus one of `boundary`,
1404
+ * `scenario`, `consumer.transitionId` or `provider.transitionId`, so
1405
+ * the rule targets a specific class of finding. "broad" allows a
1406
+ * match on kind alone or boundary alone, and such a rule also
1407
+ * silences future regressions in that category.
1408
+ */
1409
+ scope: z22.enum(["narrow", "broad"]).default("narrow"),
1410
+ /** Required human-written justification. */
1411
+ reason: z22.string().min(1),
1412
+ /** What to do when a finding matches. */
1413
+ effect: z22.enum(["mark", "downgrade", "hide"]).default("mark")
1414
+ }).strict();
1415
+ var SuppressionFileSchema = z22.object({
1416
+ version: z22.literal(1),
1417
+ rules: z22.array(SuppressionRuleSchema)
1418
+ }).strict();
1419
+
1420
+ // ../framework/rails/dist/index.js
1421
+ var ADDED_PATHS = /config\.(?:eager_load_paths|autoload_paths|autoload_once_paths)\s*(?:<<|\+=|\.push\(|\.unshift\(|\.concat\()\s*([^\n]*)/g;
1422
+ var ROOT_JOIN = /Rails\.root\.join\(\s*([^)]*)\)/g;
1423
+ var INTERPOLATED_ROOT = /#\{(?:Rails\.root|config\.root|root)\}\/([\w./-]+)/g;
1424
+ var AUTOLOAD_LIB = /config\.autoload_lib(?:_once)?\b/;
1425
+ var EVERY_LITERAL = /(["'])([^"']*)\1/g;
1426
+ function applicationFile(configDirectory) {
1427
+ const file = applicationPath(configDirectory);
1428
+ return fs.existsSync(file) ? [file] : [];
1429
+ }
1430
+ function applicationPath(configDirectory) {
1431
+ return path.join(configDirectory, "config", "application.rb");
1432
+ }
1433
+ function readAutoloadRoots(configDirectory) {
1434
+ const file = applicationPath(configDirectory);
1435
+ let source;
1436
+ try {
1437
+ source = fs.readFileSync(file, "utf8");
1438
+ } catch {
1439
+ return [];
1440
+ }
1441
+ const relative = [];
1442
+ for (const line of source.split("\n")) {
1443
+ if (line.trimStart().startsWith("#")) {
1444
+ continue;
1445
+ }
1446
+ if (AUTOLOAD_LIB.test(line)) {
1447
+ relative.push("lib");
1448
+ }
1449
+ for (const added of line.matchAll(ADDED_PATHS)) {
1450
+ relative.push(...directoriesIn(added[1] ?? ""));
1451
+ }
1452
+ }
1453
+ return [...new Set(relative)].map((directory) => path.resolve(configDirectory, directory)).filter((directory) => isDirectory(directory));
1454
+ }
1455
+ function directoriesIn(fragment) {
1456
+ const joined = [...fragment.matchAll(ROOT_JOIN)].map(
1457
+ (join) => [...(join[1] ?? "").matchAll(EVERY_LITERAL)].map((literal) => literal[2] ?? "").join("/")
1458
+ );
1459
+ const interpolated = [...fragment.matchAll(INTERPOLATED_ROOT)].map(
1460
+ (match) => match[1] ?? ""
1461
+ );
1462
+ return [...joined, ...interpolated].filter((one) => one !== "");
1463
+ }
1464
+ function isDirectory(directory) {
1465
+ try {
1466
+ return fs.statSync(directory).isDirectory();
1467
+ } catch {
1468
+ return false;
1469
+ }
1470
+ }
27
1471
  var ENGINE_BASE_NAMES = /* @__PURE__ */ new Set(["Rails::Engine", "::Rails::Engine"]);
28
1472
  var CONSTANT_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
29
1473
  function readEngines(roots, acronyms = []) {
@@ -31,7 +1475,7 @@ function readEngines(roots, acronyms = []) {
31
1475
  for (const root of roots) {
32
1476
  const routesFile = engineRoutesFile(root);
33
1477
  for (const file of engineFilesUnder(root)) {
34
- const source = fs.readFileSync(file, "utf8");
1478
+ const source = fs2.readFileSync(file, "utf8");
35
1479
  const rootNode = parseRubySync(source).rootNode;
36
1480
  for (const found of engineClassesIn(rootNode, acronyms)) {
37
1481
  engines.push({ ...found, routesFile });
@@ -48,48 +1492,45 @@ function engineSourceFiles(root) {
48
1492
  ];
49
1493
  }
50
1494
  function engineRoutesFile(root) {
51
- const routesFile = path.join(root, "config", "routes.rb");
52
- return fs.existsSync(routesFile) ? routesFile : null;
1495
+ const routesFile = path2.join(root, "config", "routes.rb");
1496
+ return fs2.existsSync(routesFile) ? routesFile : null;
53
1497
  }
54
1498
  function engineFilesUnder(root) {
55
- const directory = path.join(root, "lib");
56
- if (!fs.existsSync(directory)) {
1499
+ const directory = path2.join(root, "lib");
1500
+ if (!fs2.existsSync(directory)) {
57
1501
  return [];
58
1502
  }
59
1503
  return rubyFilesUnder(directory).filter(
60
- (file) => fs.readFileSync(file, "utf8").includes("Rails::Engine")
1504
+ (file) => fs2.readFileSync(file, "utf8").includes("Rails::Engine")
61
1505
  );
62
1506
  }
63
1507
  function expandPathPattern(pattern) {
64
- const segments = pattern.split(path.sep).filter((s) => s !== "");
65
- const start = path.isAbsolute(pattern) ? path.sep : ".";
1508
+ const segments = pattern.split(path2.sep).filter((s) => s !== "");
1509
+ const start = path2.isAbsolute(pattern) ? path2.sep : ".";
66
1510
  let candidates = [start];
67
1511
  for (const segment of segments) {
68
1512
  if (!segment.includes("*")) {
69
- candidates = candidates.map((base) => path.join(base, segment));
1513
+ candidates = candidates.map((base) => path2.join(base, segment));
70
1514
  continue;
71
1515
  }
72
1516
  const matcher = new RegExp(
73
1517
  `^${segment.split("*").map(escapeRegExp).join(".*")}$`
74
1518
  );
75
1519
  candidates = candidates.flatMap(
76
- (base) => fs.existsSync(base) ? fs.readdirSync(base).filter((name) => matcher.test(name)).sort().map((name) => path.join(base, name)) : []
1520
+ (base) => fs2.existsSync(base) ? fs2.readdirSync(base).filter((name) => matcher.test(name)).sort().map((name) => path2.join(base, name)) : []
77
1521
  );
78
1522
  }
79
- return candidates.filter((candidate) => fs.existsSync(candidate));
1523
+ return candidates.filter((candidate) => fs2.existsSync(candidate));
80
1524
  }
81
1525
  function escapeRegExp(text) {
82
1526
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
83
1527
  }
84
1528
  function rubyFilesUnder(directory) {
85
- return fs.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path.join(entry.parentPath, entry.name)).sort();
1529
+ return fs2.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path2.join(entry.parentPath, entry.name)).sort();
86
1530
  }
87
1531
  function engineClassesIn(node, acronyms, enclosing = []) {
88
1532
  const found = [];
89
- for (const child of node.namedChildren) {
90
- if (child === null) {
91
- continue;
92
- }
1533
+ for (const child of children(node)) {
93
1534
  if (child.type === "class" && extendsEngine(child)) {
94
1535
  const name = definedName(child);
95
1536
  if (name !== void 0) {
@@ -110,10 +1551,10 @@ function definedName(definition) {
110
1551
  }
111
1552
  function extendsEngine(classNode) {
112
1553
  const superclass = field(classNode, "superclass");
113
- const base = superclass?.namedChildren.find(
114
- (child) => child !== null && CONSTANT_TYPES.has(child.type)
1554
+ const base = children(superclass).find(
1555
+ (child) => CONSTANT_TYPES.has(child.type)
115
1556
  );
116
- return base !== void 0 && base !== null && ENGINE_BASE_NAMES.has(base.text);
1557
+ return base !== void 0 && ENGINE_BASE_NAMES.has(base.text);
117
1558
  }
118
1559
  function isolatedNamespaceOf(classNode, acronyms) {
119
1560
  const body = field(classNode, "body");
@@ -137,7 +1578,7 @@ var IRREGULAR_CALL = /\.irregular\s*\(?\s*(["'])([^"']*)\1\s*,\s*(["'])([^"']*)\
137
1578
  var UNCOUNTABLE_CALL = /\.uncountable\s*\(?\s*([^\n]*)/g;
138
1579
  var SINGULAR_CALL = /\.singular\s*\(?\s*(?:(["'])([^"']*)\1|\/([^/]*)\/([a-z]*))\s*,\s*(["'])([^"']*)\5/g;
139
1580
  var WORD_LIST = /%[wi][([{]([^)\]}]*)[)\]}]/;
140
- var EVERY_LITERAL = /(["'])([^"']*)\1/g;
1581
+ var EVERY_LITERAL2 = /(["'])([^"']*)\1/g;
141
1582
  var ANY_CALL = /\.(acronym|irregular|uncountable|singular)[\s("'/]/;
142
1583
  function group(match, at) {
143
1584
  return match[at] ?? "";
@@ -148,7 +1589,7 @@ function readInflections(configDirectory) {
148
1589
  const uncountable = /* @__PURE__ */ new Set();
149
1590
  const singular = [];
150
1591
  for (const file of inflectionFiles(configDirectory)) {
151
- const source = uncommented(fs2.readFileSync(file, "utf8"));
1592
+ const source = uncommented(fs3.readFileSync(file, "utf8"));
152
1593
  for (const match of source.matchAll(ACRONYM_CALL)) {
153
1594
  acronyms.add(group(match, 2));
154
1595
  }
@@ -182,18 +1623,130 @@ function wordsIn(argument) {
182
1623
  if (list !== null) {
183
1624
  return group(list, 1).split(/\s+/).filter((word) => word.length > 0);
184
1625
  }
185
- return [...argument.matchAll(EVERY_LITERAL)].map((match) => group(match, 2));
1626
+ return [...argument.matchAll(EVERY_LITERAL2)].map((match) => group(match, 2));
186
1627
  }
187
1628
  function inflectionFiles(configDirectory) {
188
- const directory = path2.join(configDirectory, "config", "initializers");
189
- if (!fs2.existsSync(directory)) {
1629
+ const directory = path3.join(configDirectory, "config", "initializers");
1630
+ if (!fs3.existsSync(directory)) {
190
1631
  return [];
191
1632
  }
192
- return fs2.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path2.join(entry.parentPath, entry.name)).filter((file) => ANY_CALL.test(uncommented(fs2.readFileSync(file, "utf8")))).sort();
1633
+ return fs3.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path3.join(entry.parentPath, entry.name)).filter((file) => ANY_CALL.test(uncommented(fs3.readFileSync(file, "utf8")))).sort();
193
1634
  }
194
1635
  function uncommented(source) {
195
1636
  return source.split("\n").filter((line) => !line.trimStart().startsWith("#")).join("\n");
196
1637
  }
1638
+ var EXPANSION_CAP = 64;
1639
+ function closingParenthesis2(text, open) {
1640
+ let depth = 0;
1641
+ for (let index = open; index < text.length; index += 1) {
1642
+ const char = text[index];
1643
+ if (char === "(") {
1644
+ depth += 1;
1645
+ }
1646
+ if (char === ")") {
1647
+ depth -= 1;
1648
+ if (depth === 0) {
1649
+ return index;
1650
+ }
1651
+ }
1652
+ }
1653
+ return -1;
1654
+ }
1655
+ function expansions(text) {
1656
+ let found = [""];
1657
+ let index = 0;
1658
+ while (index < text.length) {
1659
+ const open = text.indexOf("(", index);
1660
+ const close = text.indexOf(")", index);
1661
+ if (close !== -1 && (open === -1 || close < open)) {
1662
+ return null;
1663
+ }
1664
+ if (open === -1) {
1665
+ const rest = text.slice(index);
1666
+ found = found.map((prefix) => prefix + rest);
1667
+ break;
1668
+ }
1669
+ const end = closingParenthesis2(text, open);
1670
+ const inner = end === -1 ? null : expansions(text.slice(open + 1, end));
1671
+ if (inner === null) {
1672
+ return null;
1673
+ }
1674
+ const before = text.slice(index, open);
1675
+ const options = ["", ...inner];
1676
+ found = found.flatMap(
1677
+ (prefix) => options.map((option) => prefix + before + option)
1678
+ );
1679
+ if (found.length > EXPANSION_CAP) {
1680
+ found = found.slice(0, 1);
1681
+ }
1682
+ index = end + 1;
1683
+ }
1684
+ return found;
1685
+ }
1686
+ function tidied(path6) {
1687
+ const squeezed = `/${path6}`.replace(/\/{2,}/g, "/");
1688
+ return squeezed.length > 1 ? squeezed.replace(/\/$/, "") : squeezed;
1689
+ }
1690
+ function segmentsOf2(path6) {
1691
+ return path6 === "/" ? [] : path6.slice(1).split("/");
1692
+ }
1693
+ function sharedCount(lists, limit, fromEnd) {
1694
+ const first = lists[0] ?? [];
1695
+ const at = (list, offset) => fromEnd ? list[list.length - 1 - offset] : list[offset];
1696
+ let count = 0;
1697
+ while (count < limit && lists.every((list) => at(list, count) === at(first, count))) {
1698
+ count += 1;
1699
+ }
1700
+ return count;
1701
+ }
1702
+ var ENDS_IN_PARAMETER = /:[A-Za-z_]\w*$/;
1703
+ function headBeforeSet(lists, shortest) {
1704
+ const first = lists[0] ?? [];
1705
+ let count = sharedCount(lists, shortest, false);
1706
+ while (count > 0 && ENDS_IN_PARAMETER.test(first[count - 1] ?? "")) {
1707
+ count -= 1;
1708
+ }
1709
+ return count;
1710
+ }
1711
+ function asOnePath(paths) {
1712
+ const lists = paths.map(segmentsOf2);
1713
+ const shortest = Math.min(...lists.map((list) => list.length));
1714
+ const headCount = headBeforeSet(lists, shortest);
1715
+ const tailCount = sharedCount(lists, shortest - headCount, true);
1716
+ const first = lists[0] ?? [];
1717
+ const head = first.slice(0, headCount).join("/");
1718
+ const tail = first.slice(first.length - tailCount).join("/");
1719
+ const middles = lists.map(
1720
+ (list) => list.slice(headCount, list.length - tailCount).join("/")
1721
+ );
1722
+ if (head !== "") {
1723
+ const set2 = setPiece(middles.map((middle) => middle ? `/${middle}` : ""));
1724
+ return set2 === null ? null : `/${head}${set2}${tail ? `/${tail}` : ""}`;
1725
+ }
1726
+ const set = setPiece(
1727
+ middles.map((middle) => middle && tail ? `${middle}/` : middle)
1728
+ );
1729
+ return set === null ? null : `/${set}${tail}`;
1730
+ }
1731
+ function withGlobsAsHoles(path6) {
1732
+ return path6.replace(/(^|[/(])\*([A-Za-z_]\w*)/g, "$1:$2+");
1733
+ }
1734
+ function pathWithOptionalGroups(written) {
1735
+ const path6 = withGlobsAsHoles(written);
1736
+ if (!path6.includes("(")) {
1737
+ return path6;
1738
+ }
1739
+ const expanded = expansions(path6);
1740
+ if (expanded === null) {
1741
+ return path6;
1742
+ }
1743
+ const paths = [...new Set(expanded.map(tidied))];
1744
+ const withoutGroups = paths[0] ?? path6;
1745
+ if (paths.length === 1) {
1746
+ return withoutGroups;
1747
+ }
1748
+ return asOnePath(paths) ?? withoutGroups;
1749
+ }
197
1750
  function readSimpleArgs(call, defaults = {}) {
198
1751
  const argumentList = field2(call, "arguments");
199
1752
  const positional = [];
@@ -261,23 +1814,33 @@ function readSymbolList(node) {
261
1814
  const single = symbolValue(node);
262
1815
  return single === null ? null : [single];
263
1816
  }
1817
+ var updateAt = (path6) => [
1818
+ { method: "PATCH", path: path6 },
1819
+ { method: "PUT", path: path6 }
1820
+ ];
264
1821
  var RESTFUL_ROUTES_PLURAL = {
265
- index: (base) => ({ method: "GET", path: base }),
266
- create: (base) => ({ method: "POST", path: base }),
267
- new: (base) => ({ method: "GET", path: `${base}/new` }),
268
- show: (base) => ({ method: "GET", path: `${base}/:id` }),
269
- edit: (base) => ({ method: "GET", path: `${base}/:id/edit` }),
270
- update: (base) => ({ method: "PATCH", path: `${base}/:id` }),
271
- destroy: (base) => ({ method: "DELETE", path: `${base}/:id` })
1822
+ index: (base) => [{ method: "GET", path: base }],
1823
+ create: (base) => [{ method: "POST", path: base }],
1824
+ new: (base) => [{ method: "GET", path: `${base}/new` }],
1825
+ show: (base) => [{ method: "GET", path: `${base}/:id` }],
1826
+ edit: (base) => [{ method: "GET", path: `${base}/:id/edit` }],
1827
+ update: (base) => updateAt(`${base}/:id`),
1828
+ destroy: (base) => [{ method: "DELETE", path: `${base}/:id` }]
272
1829
  };
273
1830
  var RESTFUL_ROUTES_SINGULAR = {
274
- create: (base) => ({ method: "POST", path: base }),
275
- new: (base) => ({ method: "GET", path: `${base}/new` }),
276
- show: (base) => ({ method: "GET", path: base }),
277
- edit: (base) => ({ method: "GET", path: `${base}/edit` }),
278
- update: (base) => ({ method: "PATCH", path: base }),
279
- destroy: (base) => ({ method: "DELETE", path: base })
1831
+ create: (base) => [{ method: "POST", path: base }],
1832
+ new: (base) => [{ method: "GET", path: `${base}/new` }],
1833
+ show: (base) => [{ method: "GET", path: base }],
1834
+ edit: (base) => [{ method: "GET", path: `${base}/edit` }],
1835
+ update: (base) => updateAt(base),
1836
+ destroy: (base) => [{ method: "DELETE", path: base }]
280
1837
  };
1838
+ function restfulRoutes(action, base) {
1839
+ if (!Object.hasOwn(RESTFUL_ROUTES_PLURAL, action)) {
1840
+ return [];
1841
+ }
1842
+ return RESTFUL_ROUTES_PLURAL[action]?.(base) ?? [];
1843
+ }
281
1844
  function pluralize(word) {
282
1845
  if (/[^aeiou]y$/.test(word)) {
283
1846
  return `${word.slice(0, -1)}ies`;
@@ -317,12 +1880,19 @@ var RouteAccumulator = class {
317
1880
  mounting = /* @__PURE__ */ new Set();
318
1881
  /** The file whose statements are being walked, as a gap message prints it. */
319
1882
  file = "";
320
- /** The first route written for an action wins, because Rails matches routes in the order they are declared. */
1883
+ /** An action routed from several places serves every one of them, so each distinct route is kept. */
321
1884
  add(controllerKey, action, route) {
322
1885
  const key = `${controllerKey}#${action}`;
323
- if (!this.byKey.has(key)) {
324
- this.byKey.set(key, route);
1886
+ const routes = this.byKey.get(key) ?? [];
1887
+ const path6 = pathWithOptionalGroups(route.path);
1888
+ if (!routes.some((one) => one.method === route.method && one.path === path6)) {
1889
+ routes.push({
1890
+ method: route.method,
1891
+ path: path6,
1892
+ ...route.formatless === true ? { formatless: true } : {}
1893
+ });
325
1894
  }
1895
+ this.byKey.set(key, routes);
326
1896
  }
327
1897
  recordUnread(callName) {
328
1898
  addUnderFile(this.unread, this.file, callName);
@@ -333,8 +1903,8 @@ var RouteAccumulator = class {
333
1903
  recordMissingDrawn(name) {
334
1904
  this.missingDrawn.push({ file: this.file, name });
335
1905
  }
336
- routeFor(controllerKey, action) {
337
- return this.byKey.get(`${controllerKey}#${action}`) ?? null;
1906
+ routesFor(controllerKey, action) {
1907
+ return this.byKey.get(`${controllerKey}#${action}`) ?? [];
338
1908
  }
339
1909
  /** Runs `walk` with gaps attributed to `file`, then goes back to the file being walked before. */
340
1910
  inFile(file, walk) {
@@ -359,7 +1929,7 @@ var RouteAccumulator = class {
359
1929
  }
360
1930
  for (const { file, name } of this.missingDrawn) {
361
1931
  gaps.push(
362
- `${file} draws ${name}, but there is no ${name}.rb under ${path3.basename(this.drawDirectory)}/ beside it to read; whatever that file routes is missing from what suss reports`
1932
+ `${file} draws ${name}, but there is no ${name}.rb under ${path4.basename(this.drawDirectory)}/ beside it to read; whatever that file routes is missing from what suss reports`
363
1933
  );
364
1934
  }
365
1935
  return gaps;
@@ -396,14 +1966,16 @@ function handleResourceCall(call, ctx, out, plural) {
396
1966
  const only = readSymbolList(args.keyword.only);
397
1967
  const except = readSymbolList(args.keyword.except);
398
1968
  const table = plural ? RESTFUL_ROUTES_PLURAL : RESTFUL_ROUTES_SINGULAR;
399
- for (const [action, routeAt] of Object.entries(table)) {
1969
+ for (const [action, routesAt] of Object.entries(table)) {
400
1970
  if (only !== null && !only.includes(action)) {
401
1971
  continue;
402
1972
  }
403
1973
  if (except?.includes(action)) {
404
1974
  continue;
405
1975
  }
406
- out.add(controllerKey, action, routeAt(base));
1976
+ for (const route of routesAt(base)) {
1977
+ out.add(controllerKey, action, route);
1978
+ }
407
1979
  }
408
1980
  const nestedBase = plural ? `${base}/:${singularize(symbol)}_id` : base;
409
1981
  const nested = {
@@ -447,13 +2019,15 @@ function handleVerb(call, ctx, out, method) {
447
2019
  const on = wordValue(args.keyword.on, ctx);
448
2020
  const base = ctx.resource === void 0 ? ctx.pathPrefix : baseForOn(ctx.resource, on);
449
2021
  const target = readRouteTarget(args, ctx);
2022
+ const format = formatOf(args);
450
2023
  if (target !== null) {
451
2024
  const literalPath = args.positional[0] ? textValue(args.positional[0], ctx) : null;
452
- const path5 = target.path ?? literalPath;
453
- if (path5 !== null) {
2025
+ const path6 = target.path ?? literalPath;
2026
+ if (path6 !== null) {
454
2027
  out.add(joinKey(ctx.modulePrefix, target.controllerKey), target.action, {
455
2028
  method,
456
- path: joinPath(base, path5)
2029
+ path: joinPath(base, path6),
2030
+ ...format
457
2031
  });
458
2032
  }
459
2033
  return;
@@ -468,7 +2042,14 @@ function handleVerb(call, ctx, out, method) {
468
2042
  if (controllerKey === void 0) {
469
2043
  return;
470
2044
  }
471
- out.add(controllerKey, action, { method, path: joinPath(base, segment) });
2045
+ out.add(controllerKey, action, {
2046
+ method,
2047
+ path: joinPath(base, segment),
2048
+ ...format
2049
+ });
2050
+ }
2051
+ function formatOf(args) {
2052
+ return args.keyword.format?.type === "false" ? { formatless: true } : {};
472
2053
  }
473
2054
  function handleRoot(call, ctx, out) {
474
2055
  const args = readSimpleArgs(call, ctx.defaults);
@@ -598,9 +2179,9 @@ function handleMatch(call, ctx, out) {
598
2179
  }
599
2180
  }
600
2181
  function drawDirectoryOf(routesFile) {
601
- return path3.join(
602
- path3.dirname(routesFile),
603
- path3.basename(routesFile, path3.extname(routesFile))
2182
+ return path4.join(
2183
+ path4.dirname(routesFile),
2184
+ path4.basename(routesFile, path4.extname(routesFile))
604
2185
  );
605
2186
  }
606
2187
  function handleDraw(call, ctx, out) {
@@ -609,16 +2190,16 @@ function handleDraw(call, ctx, out) {
609
2190
  if (name === null) {
610
2191
  return;
611
2192
  }
612
- const file = path3.join(out.drawDirectory, `${name}.rb`);
2193
+ const file = path4.join(out.drawDirectory, `${name}.rb`);
613
2194
  if (out.drawn.has(file)) {
614
2195
  return;
615
2196
  }
616
2197
  out.drawn.add(file);
617
- if (!fs3.existsSync(file)) {
2198
+ if (!fs4.existsSync(file)) {
618
2199
  out.recordMissingDrawn(name);
619
2200
  return;
620
2201
  }
621
- const tree = parseRubySync2(fs3.readFileSync(file, "utf8"));
2202
+ const tree = parseRubySync2(fs4.readFileSync(file, "utf8"));
622
2203
  walkBody(tree.rootNode, ctx, out);
623
2204
  }
624
2205
  var HANDLERS = {
@@ -726,10 +2307,8 @@ function routeDeclarations(root, displayPath) {
726
2307
  found.push(declaration);
727
2308
  return;
728
2309
  }
729
- for (const child of node.namedChildren) {
730
- if (child !== null) {
731
- visit(child);
732
- }
2310
+ for (const child of children2(node)) {
2311
+ visit(child);
733
2312
  }
734
2313
  };
735
2314
  visit(root);
@@ -756,16 +2335,16 @@ function isApplication(owner) {
756
2335
  return owner === "Rails.application" || /(^|::)Application$/.test(owner);
757
2336
  }
758
2337
  function parseDeclarations(source) {
759
- if (!fs3.existsSync(source.file)) {
2338
+ if (!fs4.existsSync(source.file)) {
760
2339
  return [];
761
2340
  }
762
- const tree = parseRubySync2(fs3.readFileSync(source.file, "utf8"));
2341
+ const tree = parseRubySync2(fs4.readFileSync(source.file, "utf8"));
763
2342
  return routeDeclarations(tree.rootNode, source.displayPath);
764
2343
  }
765
2344
  function readRoutes(input) {
766
- if (!fs3.existsSync(input.routesFile.file)) {
2345
+ if (!fs4.existsSync(input.routesFile.file)) {
767
2346
  return {
768
- routeFor: () => null,
2347
+ routesFor: () => [],
769
2348
  fileFound: false,
770
2349
  gaps: [
771
2350
  `${input.routesFile.displayPath} does not exist, so this run assumes each action's path and method from Rails' RESTful naming convention instead of reading it from routing`
@@ -806,7 +2385,7 @@ function readRoutes(input) {
806
2385
  });
807
2386
  }
808
2387
  return {
809
- routeFor: (controllerKey, action) => out.routeFor(controllerKey, action),
2388
+ routesFor: (controllerKey, action) => out.routesFor(controllerKey, action),
810
2389
  fileFound: true,
811
2390
  gaps: out.gaps()
812
2391
  };
@@ -879,65 +2458,127 @@ var RACK_STATUS_CODE_NAMES = {
879
2458
  not_extended: 510,
880
2459
  network_authentication_required: 511
881
2460
  };
882
- var optionsSchema = z.object({
2461
+ var STANDARD_ERROR = ["StandardError", "Exception"];
2462
+ var ACTION_CONTROLLER_ERROR = [
2463
+ "ActionController::ActionControllerError",
2464
+ ...STANDARD_ERROR
2465
+ ];
2466
+ var ACTIVE_RECORD_ERROR = [
2467
+ "ActiveRecord::ActiveRecordError",
2468
+ ...STANDARD_ERROR
2469
+ ];
2470
+ var INDEX_ERROR = ["IndexError", ...STANDARD_ERROR];
2471
+ var LIBRARY_EXCEPTIONS = {
2472
+ "ActionController::ActionControllerError": { ancestors: STANDARD_ERROR },
2473
+ "ActionController::BadRequest": {
2474
+ status: 400,
2475
+ ancestors: ACTION_CONTROLLER_ERROR
2476
+ },
2477
+ "ActionController::ParameterMissing": {
2478
+ status: 400,
2479
+ ancestors: ["KeyError", ...INDEX_ERROR]
2480
+ },
2481
+ "ActionController::UnpermittedParameters": { ancestors: INDEX_ERROR },
2482
+ "ActionController::RoutingError": {
2483
+ status: 404,
2484
+ ancestors: ACTION_CONTROLLER_ERROR
2485
+ },
2486
+ "AbstractController::ActionNotFound": {
2487
+ status: 404,
2488
+ ancestors: STANDARD_ERROR
2489
+ },
2490
+ "ActionController::MethodNotAllowed": {
2491
+ status: 405,
2492
+ ancestors: ACTION_CONTROLLER_ERROR
2493
+ },
2494
+ "ActionController::NotImplemented": {
2495
+ status: 501,
2496
+ ancestors: [
2497
+ "ActionController::MethodNotAllowed",
2498
+ ...ACTION_CONTROLLER_ERROR
2499
+ ]
2500
+ },
2501
+ "ActionController::UnknownFormat": {
2502
+ status: 406,
2503
+ ancestors: ACTION_CONTROLLER_ERROR
2504
+ },
2505
+ "ActionController::InvalidAuthenticityToken": {
2506
+ status: 422,
2507
+ ancestors: ACTION_CONTROLLER_ERROR
2508
+ },
2509
+ "ActiveRecord::ActiveRecordError": { ancestors: STANDARD_ERROR },
2510
+ "ActiveRecord::RecordNotFound": {
2511
+ status: 404,
2512
+ ancestors: ACTIVE_RECORD_ERROR
2513
+ },
2514
+ "ActiveRecord::RecordInvalid": {
2515
+ status: 422,
2516
+ ancestors: ACTIVE_RECORD_ERROR
2517
+ },
2518
+ "ActiveRecord::RecordNotSaved": {
2519
+ status: 422,
2520
+ ancestors: ACTIVE_RECORD_ERROR
2521
+ },
2522
+ "ActiveRecord::StaleObjectError": {
2523
+ status: 409,
2524
+ ancestors: ACTIVE_RECORD_ERROR
2525
+ }
2526
+ };
2527
+ var optionsSchema = z15.object({
883
2528
  /**
884
2529
  * The directory a bare controller name is looked up under, `app` in a
885
2530
  * scaffolded Rails app.
886
2531
  */
887
- root: z.string().optional(),
2532
+ root: z15.string().optional(),
888
2533
  /**
889
2534
  * The CLI fills this in with the config file's directory, or with the
890
2535
  * directory the run reads when there is no file. A relative `root` or
891
2536
  * `routesFile` resolves against it. A config file does not set it.
892
2537
  */
893
- configDirectory: z.string().optional(),
2538
+ configDirectory: z15.string().optional(),
894
2539
  /**
895
2540
  * Base classes beyond `ApplicationController` whose subclasses are
896
2541
  * controllers. A config file may not set this; a dependency stub
897
2542
  * fills it.
898
2543
  */
899
- baseClassNames: z.array(z.string()).optional(),
2544
+ baseClassNames: z15.array(z15.string()).optional(),
900
2545
  /**
901
2546
  * Controller methods a gem such as Devise adds, beyond the ones Rails
902
2547
  * defines. They are left off an action's effects the same way Rails'
903
2548
  * own are.
904
2549
  */
905
- inheritedMethodNames: z.array(z.string()).optional(),
2550
+ inheritedMethodNames: z15.array(z15.string()).optional(),
906
2551
  /**
907
2552
  * The routes file, `config/routes.rb` in a scaffolded app. A relative
908
2553
  * path resolves against `configDirectory` when there is one.
909
2554
  */
910
- routesFile: z.string().optional(),
2555
+ routesFile: z15.string().optional(),
911
2556
  /**
912
2557
  * Directories that each contain an engine, with its `Rails::Engine`
913
2558
  * class under `lib/` and its routes in `config/routes.rb`. A `*` in a
914
2559
  * segment matches any one directory, so `plugins/*` covers every
915
2560
  * plugin. Relative paths resolve against `configDirectory`.
916
2561
  */
917
- engineRoots: z.array(z.string()).optional(),
2562
+ engineRoots: z15.array(z15.string()).optional(),
918
2563
  /**
919
2564
  * Other files that add routes through a `Rails.application.routes.draw`,
920
2565
  * `.append` or `.prepend` block, most often a plugin's `plugin.rb`.
921
2566
  * They take `*` the same way `engineRoots` does, and relative paths
922
2567
  * resolve against `configDirectory`.
923
2568
  */
924
- routesFiles: z.array(z.string()).optional()
2569
+ routesFiles: z15.array(z15.string()).optional()
925
2570
  }).strict();
926
2571
  var RAILS_ROOT_CLASS_NAMES = [
927
2572
  "ActionController::Base",
928
2573
  "ActionController::API"
929
2574
  ];
930
- var RESTFUL_ACTIONS = {
931
- index: { method: "GET", pathTemplate: "/:resource" },
932
- show: { method: "GET", pathTemplate: "/:resource/:id" },
933
- new: { method: "GET", pathTemplate: "/:resource/new" },
934
- create: { method: "POST", pathTemplate: "/:resource" },
935
- edit: { method: "GET", pathTemplate: "/:resource/:id/edit" },
936
- update: { method: "PATCH", pathTemplate: "/:resource/:id" },
937
- destroy: { method: "DELETE", pathTemplate: "/:resource/:id" }
938
- };
939
2575
  var RESPONSE_STATUS_CALLS = [
940
- { name: "render", statusKeyword: "status" },
2576
+ {
2577
+ name: "render",
2578
+ statusKeyword: "status",
2579
+ bodyKeyword: "json",
2580
+ bodySerializers: ["to_json", "as_json"]
2581
+ },
941
2582
  { name: "head", statusArgument: 0, statusKeyword: "status" },
942
2583
  { name: "redirect_to", statusKeyword: "status", defaultStatusCode: 302 },
943
2584
  { name: "redirect_back", statusKeyword: "status", defaultStatusCode: 302 }
@@ -993,22 +2634,15 @@ function controllerKeyFromQualified(qualifiedName, acronyms) {
993
2634
  const withoutSuffix = qualifiedName.endsWith("Controller") ? qualifiedName.slice(0, -"Controller".length) : qualifiedName;
994
2635
  return underscoreConstantPath2(withoutSuffix, acronyms);
995
2636
  }
996
- function conventionalRoute(controllerKey, actionName) {
997
- const template = RESTFUL_ACTIONS[actionName];
998
- if (template === void 0) {
999
- return null;
1000
- }
2637
+ function conventionalRoutes(controllerKey, actionName) {
1001
2638
  const resource = controllerKey.split("/").pop() ?? controllerKey;
1002
- return {
1003
- method: template.method,
1004
- path: template.pathTemplate.replace(":resource", resource)
1005
- };
2639
+ return restfulRoutes(actionName, `/${resource}`);
1006
2640
  }
1007
2641
  function resolveAgainst(configDirectory, value) {
1008
- if (configDirectory === void 0 || path4.isAbsolute(value)) {
2642
+ if (configDirectory === void 0 || path5.isAbsolute(value)) {
1009
2643
  return value;
1010
2644
  }
1011
- return path4.resolve(configDirectory, value);
2645
+ return path5.resolve(configDirectory, value);
1012
2646
  }
1013
2647
  function railsFramework(options = {}) {
1014
2648
  const root = resolveAgainst(options.configDirectory, options.root ?? "app");
@@ -1016,7 +2650,7 @@ function railsFramework(options = {}) {
1016
2650
  options.configDirectory,
1017
2651
  options.routesFile ?? "config/routes.rb"
1018
2652
  );
1019
- const displayPathOf = (file) => options.configDirectory === void 0 ? file : path4.relative(options.configDirectory, file);
2653
+ const displayPathOf = (file) => options.configDirectory === void 0 ? file : path5.relative(options.configDirectory, file);
1020
2654
  const engineRoots = () => expandPatterns(options.configDirectory, options.engineRoots);
1021
2655
  const extraRoutesFiles = () => expandPatterns(options.configDirectory, options.routesFiles);
1022
2656
  const inflections = options.configDirectory === void 0 ? EMPTY_INFLECTIONS : readInflections(options.configDirectory);
@@ -1047,24 +2681,28 @@ function railsFramework(options = {}) {
1047
2681
  ...options.baseClassNames ?? []
1048
2682
  ],
1049
2683
  root,
2684
+ autoloadRoots: options.configDirectory === void 0 ? [] : readAutoloadRoots(options.configDirectory),
1050
2685
  pathConvention: "railsUnderscore",
1051
2686
  acronyms,
1052
2687
  ancestryRootClassNames: [...RAILS_ROOT_CLASS_NAMES],
1053
2688
  defaultStatusCode: 200,
1054
2689
  responseStatusCalls: RESPONSE_STATUS_CALLS,
1055
2690
  statusCodeNames: RACK_STATUS_CODE_NAMES,
2691
+ formatSwitch: { name: "respond_to", matchesEvery: "any" },
2692
+ libraryExceptions: LIBRARY_EXCEPTIONS,
1056
2693
  inheritedMethodNames: [
1057
2694
  ...RAILS_CONTROLLER_METHODS,
1058
2695
  ...options.inheritedMethodNames ?? []
1059
2696
  ],
1060
2697
  routesFile,
1061
2698
  filters: CONTROLLER_FILTERS,
1062
- routeFor: (controllerQualifiedName, actionName) => {
2699
+ routesFor: (controllerQualifiedName, actionName) => {
1063
2700
  const key = controllerKeyFromQualified(controllerQualifiedName, acronyms);
1064
2701
  const found = routeTable();
1065
- return found.fileFound ? found.routeFor(key, actionName) : conventionalRoute(key, actionName);
2702
+ return found.fileFound ? found.routesFor(key, actionName) : conventionalRoutes(key, actionName);
1066
2703
  },
1067
- routingGaps: () => routeTable().gaps
2704
+ routingGaps: () => routeTable().gaps,
2705
+ optionalFormat: true
1068
2706
  };
1069
2707
  return {
1070
2708
  name: "rails",
@@ -1089,7 +2727,10 @@ function railsFramework(options = {}) {
1089
2727
  ...drawableRoutesFiles(routesFile),
1090
2728
  ...engineRoots().flatMap(engineSourceFiles),
1091
2729
  ...extraRoutesFiles(),
1092
- ...options.configDirectory === void 0 ? [] : inflectionFiles(options.configDirectory)
2730
+ ...options.configDirectory === void 0 ? [] : [
2731
+ ...inflectionFiles(options.configDirectory),
2732
+ ...applicationFile(options.configDirectory)
2733
+ ]
1093
2734
  ]
1094
2735
  };
1095
2736
  }
@@ -1100,10 +2741,10 @@ function expandPatterns(configDirectory, patterns) {
1100
2741
  }
1101
2742
  function drawableRoutesFiles(routesFile) {
1102
2743
  const directory = drawDirectoryOf(routesFile);
1103
- if (!fs4.existsSync(directory)) {
2744
+ if (!fs5.existsSync(directory)) {
1104
2745
  return [];
1105
2746
  }
1106
- return fs4.readdirSync(directory).filter((name) => name.endsWith(".rb")).sort().map((name) => path4.join(directory, name));
2747
+ return fs5.readdirSync(directory).filter((name) => name.endsWith(".rb")).sort().map((name) => path5.join(directory, name));
1107
2748
  }
1108
2749
  var declares = {
1109
2750
  kind: "framework",