@suss/extractor 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -245,892 +245,799 @@ function mergeInputReads(derived, syntactic) {
245
245
  }
246
246
 
247
247
  // src/composeWrappers.ts
248
- var import_behavioral_ir3 = require("@suss/behavioral-ir");
249
-
250
- // src/paths/enumeratePaths.ts
251
248
  var import_behavioral_ir2 = require("@suss/behavioral-ir");
252
- var MAX_PATHS = 256;
253
- var UnmodeledFlow = class extends Error {
254
- };
255
- var PathBudgetExceeded = class extends Error {
256
- };
257
- function branchCond(branchStmt, condition, polarity, oppositeExit) {
258
- return {
259
- info: {
260
- sourceText: condition.sourceText,
261
- polarity,
262
- source: "explicit",
263
- expression: condition.expression
264
- },
265
- branchStmt,
266
- oppositeExit
267
- };
249
+ function composeWrappers(summaries, options = {}) {
250
+ const chain = (0, import_behavioral_ir2.wrapperIndex)(summaries);
251
+ const keepGaps = options.gapHandling !== "silent";
252
+ return summaries.map((summary) => composeOne(summary, chain, keepGaps));
268
253
  }
269
- function fixedCond(condition, polarity, source) {
270
- return {
271
- info: {
272
- sourceText: condition.sourceText,
273
- polarity,
274
- source,
275
- expression: condition.expression
276
- },
277
- branchStmt: null,
278
- oppositeExit: null
254
+ function composeOne(summary, chain, keepGaps) {
255
+ const recorded = (0, import_behavioral_ir2.readWrapperMetadata)(summary);
256
+ const applied = recorded?.applied ?? [];
257
+ const covering = applied.filter(
258
+ (reference) => coversUnit(reference, summary)
259
+ );
260
+ const narrowed = covering.length === applied.length ? summary : {
261
+ ...summary,
262
+ metadata: (0, import_behavioral_ir2.withWrapperMetadata)(summary.metadata, {
263
+ applied: covering
264
+ })
279
265
  };
280
- }
281
- var loopIterationCond = (headerText) => fixedCond(
282
- { sourceText: `some iteration of: ${headerText}`, expression: null },
283
- "positive",
284
- "explicit"
285
- );
286
- var loopCompletedCond = (headerText, exit) => fixedCond(
287
- {
288
- sourceText: `loop exited via ${exit ?? "return"}: ${headerText}`,
289
- expression: null
290
- },
291
- "negative",
292
- exit === "throw" ? "earlyThrow" : "earlyReturn"
293
- );
294
- var catchEntryCond = () => fixedCond(
295
- { sourceText: import_behavioral_ir2.CATCH_ENTRY_TEXT, expression: null },
296
- "positive",
297
- "catchBlock"
298
- );
299
- function classify(path4, home, isAncestorOrSelf) {
300
- return path4.map((cond) => {
301
- if (cond.branchStmt === null) {
302
- return cond.info;
303
- }
304
- const encloses = home !== null && isAncestorOrSelf(cond.branchStmt, home);
305
- if (encloses || cond.info.polarity === "positive" || cond.oppositeExit === null) {
306
- return { ...cond.info, source: "explicit" };
307
- }
308
- const source = cond.oppositeExit === "throw" ? "earlyThrow" : "earlyReturn";
309
- return { ...cond.info, source };
266
+ const wrappers = covering.flatMap((reference) => {
267
+ const found = (0, import_behavioral_ir2.wrapperFor)(chain, reference);
268
+ return found === void 0 ? [] : [{ reference, summary: found }];
310
269
  });
311
- }
312
- function childrenOf(stmt) {
313
- return [...ownBlocksOf(stmt), ...(stmt.callbacks ?? []).flat()];
314
- }
315
- function ownBlocksOf(stmt) {
316
- if (stmt.kind === "if") {
317
- return stmt.elseBody === null ? stmt.thenBody : [...stmt.thenBody, ...stmt.elseBody];
270
+ if (wrappers.length === 0) {
271
+ return narrowed;
318
272
  }
319
- if (stmt.kind === "switch") {
320
- return stmt.groups.flatMap((g) => g.body);
273
+ const responses = respondedInstead(wrappers);
274
+ const handled = handledThrows(wrappers);
275
+ const transitions = beside(summary, responses, handled);
276
+ if (!keepGaps) {
277
+ return transitions === summary.transitions ? narrowed : { ...narrowed, transitions };
321
278
  }
322
- if (stmt.kind === "loop") {
323
- return stmt.body;
279
+ const gaps = withContractStatusGaps(
280
+ summary,
281
+ [...responses, ...summary.transitions],
282
+ handled
283
+ );
284
+ if (transitions === summary.transitions && gaps === summary.gaps) {
285
+ return narrowed;
324
286
  }
325
- if (stmt.kind === "try") {
326
- return [
327
- ...stmt.tryBody,
328
- ...stmt.catchBody ?? [],
329
- ...stmt.finallyBody ?? []
330
- ];
287
+ return { ...narrowed, transitions, gaps };
288
+ }
289
+ function beside(summary, responses, handled) {
290
+ const throws = summary.transitions.some((t) => t.output.type === "throw");
291
+ const onThrow = throws ? handled : [];
292
+ if (responses.length === 0 && onThrow.length === 0) {
293
+ return summary.transitions;
331
294
  }
332
- return [];
295
+ return withDistinctIds([...responses, ...summary.transitions, ...onThrow]);
333
296
  }
334
- function hasStrayBreak(nodes) {
335
- return nodes.some((node) => {
336
- if (node.kind === "exit" && node.exit === "break") {
337
- return true;
338
- }
339
- if (node.kind === "switch" || node.kind === "loop") {
340
- return false;
341
- }
342
- return hasStrayBreak(ownBlocksOf(node));
343
- });
297
+ function respondedInstead(wrappers) {
298
+ return wrappers.flatMap(
299
+ (wrapper) => wrapper.reference.onThrow === true ? [] : attribute(
300
+ wrapper.summary.transitions.filter(
301
+ (t) => t.output.type !== "delegate"
302
+ ),
303
+ wrapper.reference
304
+ )
305
+ );
344
306
  }
345
- function exitKindOfList(stmts) {
346
- let sawReturn = false;
347
- for (const stmt of stmts) {
348
- if (stmt.exitKind === "throw") {
349
- return "throw";
350
- }
351
- if (stmt.exitKind === "return") {
352
- sawReturn = true;
353
- }
354
- }
355
- return sawReturn ? "return" : null;
307
+ function handledThrows(wrappers) {
308
+ return wrappers.flatMap(
309
+ (wrapper) => wrapper.reference.onThrow === true ? attribute(wrapper.summary.transitions, wrapper.reference) : []
310
+ );
356
311
  }
357
- function chargeBudget(state) {
358
- state.pathCount++;
359
- if (state.pathCount > MAX_PATHS) {
360
- throw new PathBudgetExceeded(
361
- `path budget exceeded, more than ${MAX_PATHS} paths`
362
- );
312
+ function withContractStatusGaps(summary, composed, handled) {
313
+ const contract = (0, import_behavioral_ir2.readHttpMetadata)(summary)?.declaredContract;
314
+ const binding = summary.identity.boundaryBinding;
315
+ if (contract === void 0 || binding !== null && !(0, import_behavioral_ir2.exchangesHttpResponses)(binding)) {
316
+ return summary.gaps;
363
317
  }
318
+ const declared = {
319
+ framework: contract.framework ?? "declared",
320
+ responses: contract.responses
321
+ };
322
+ const stale = new Set(
323
+ contractStatusGaps(declared, summary.transitions).map(gapKey)
324
+ );
325
+ const gaps = [
326
+ ...summary.gaps.filter((gap) => !stale.has(gapKey(gap))),
327
+ ...contractStatusGaps(declared, [...composed, ...handled])
328
+ ];
329
+ const unchanged = gaps.length === summary.gaps.length && gaps.every((gap, i) => gapKey(gap) === gapKey(summary.gaps[i]));
330
+ return unchanged ? summary.gaps : gaps;
364
331
  }
365
- function recordTerminal(ctx, terminal, home, path4) {
366
- chargeBudget(ctx.state);
367
- const info = classify(path4, home, ctx.isAncestorOrSelf);
368
- const existing = ctx.state.byTerminal.get(terminal) ?? [];
369
- existing.push(info);
370
- ctx.state.byTerminal.set(terminal, existing);
332
+ function gapKey(gap) {
333
+ return `${gap.type}|${gap.description}`;
371
334
  }
372
- function containsTerminal(ctx, stmt) {
373
- if ((ctx.terminalsByStmt.get(stmt)?.length ?? 0) > 0) {
335
+ function coversUnit(reference, summary) {
336
+ if (reference.scope === void 0) {
374
337
  return true;
375
338
  }
376
- return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
339
+ const binding = summary.identity.boundaryBinding;
340
+ return binding !== null && (0, import_behavioral_ir2.withinScope)(binding, reference.scope);
377
341
  }
378
- function stepIf(stmt, path4, ctx) {
379
- const thenExit = exitKindOfList(stmt.thenBody);
380
- const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
381
- const thenPath = [
382
- ...path4,
383
- branchCond(stmt, stmt.condition, "positive", elseExit)
384
- ];
385
- const elsePath = [
386
- ...path4,
387
- branchCond(
388
- stmt,
389
- stmt.condition,
390
- "negative",
391
- stmt.elseBody === null ? thenExit : null
392
- )
393
- ];
394
- const out = [];
395
- out.push(...enumerate(ctx, stmt.thenBody, thenPath));
396
- if (stmt.elseBody !== null) {
397
- out.push(...enumerate(ctx, stmt.elseBody, elsePath));
398
- } else {
399
- out.push(elsePath);
342
+ function attribute(transitions, reference) {
343
+ return transitions.map((transition) => ({
344
+ ...transition,
345
+ metadata: (0, import_behavioral_ir2.withWrapperMetadata)(transition.metadata, { from: reference })
346
+ }));
347
+ }
348
+ function withDistinctIds(transitions) {
349
+ const seen = /* @__PURE__ */ new Map();
350
+ return transitions.map((transition) => {
351
+ const taken = seen.get(transition.id);
352
+ seen.set(transition.id, (taken ?? 0) + 1);
353
+ return taken === void 0 ? transition : { ...transition, id: `${transition.id}#${taken + 1}` };
354
+ });
355
+ }
356
+
357
+ // src/effectGuards.ts
358
+ var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
359
+ var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
360
+ function guardsHoldOn(preconditions, conditions) {
361
+ if (preconditions === void 0 || preconditions.length === 0) {
362
+ return true;
400
363
  }
401
- return out;
364
+ const onBranch = new Set(conditions.map(keyOf));
365
+ return preconditions.every(
366
+ (precondition) => !onBranch.has(opposite(precondition))
367
+ );
402
368
  }
403
- function stepSwitch(stmt, path4, ctx) {
404
- const out = [];
405
- const negations = [];
406
- let defaultRanBody = false;
407
- for (let i = 0; i < stmt.groups.length; i++) {
408
- const group = stmt.groups[i];
409
- const fallthroughSafe = i === stmt.groups.length - 1;
410
- if (hasStrayBreak(group.body)) {
411
- throw new UnmodeledFlow("non-trailing break in switch clause");
412
- }
413
- const groupPath = group.condition === null ? [...path4, ...negations] : [...path4, fixedCond(group.condition, "positive", "explicit")];
414
- const conts = enumerate(ctx, group.body, groupPath);
415
- if (conts.length > 0) {
416
- if (!group.hasTrailingBreak && !fallthroughSafe) {
417
- throw new UnmodeledFlow("fallthrough into a non-empty switch clause");
418
- }
419
- out.push(...conts);
369
+ function runsBefore(effectLine, terminalEndLine) {
370
+ return effectLine <= terminalEndLine;
371
+ }
372
+
373
+ // src/moduleImports.ts
374
+ function stampModuleImports(summaries, importsOf) {
375
+ const byFile = /* @__PURE__ */ new Map();
376
+ for (const summary of summaries) {
377
+ const file = summary.location.file;
378
+ if (!byFile.has(file)) {
379
+ const found = importsOf(file);
380
+ byFile.set(
381
+ file,
382
+ found === void 0 ? void 0 : [...new Set(found)].sort()
383
+ );
420
384
  }
421
- if (group.condition === null) {
422
- defaultRanBody = true;
423
- } else {
424
- const exit = exitKindOfList(group.body);
425
- const negationSource = exit === "throw" ? "earlyThrow" : exit === "return" ? "earlyReturn" : "explicit";
426
- negations.push(fixedCond(group.condition, "negative", negationSource));
385
+ const imports = byFile.get(file);
386
+ if (imports === void 0) {
387
+ continue;
427
388
  }
389
+ summary.metadata = {
390
+ ...summary.metadata ?? {},
391
+ moduleImports: imports
392
+ };
428
393
  }
429
- if (!defaultRanBody) {
430
- out.push([...path4, ...negations]);
431
- }
432
- return out;
433
394
  }
434
- function stepLoop(stmt, path4, ctx) {
435
- const bodyExit = exitKindOfList(stmt.body);
436
- if (containsTerminal(ctx, stmt)) {
437
- enumerate(ctx, stmt.body, [
438
- ...path4,
439
- loopIterationCond(stmt.condition.sourceText)
440
- ]);
441
- }
442
- if (bodyExit !== null) {
443
- return [[...path4, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
444
- }
445
- return [path4];
446
- }
447
- function stepTry(stmt, path4, ctx) {
448
- if (stmt.finallyBody !== null) {
449
- const finallyHasTerminal = stmt.finallyBody.some(
450
- (s) => containsTerminal(ctx, s)
451
- );
452
- if (exitKindOfList(stmt.finallyBody) !== null || finallyHasTerminal) {
453
- throw new UnmodeledFlow("finally block with exits or terminals");
454
- }
455
- }
456
- const out = [];
457
- out.push(...enumerate(ctx, stmt.tryBody, path4));
458
- if (stmt.catchBody !== null) {
459
- const catchPath = [...path4, catchEntryCond()];
460
- out.push(...enumerate(ctx, stmt.catchBody, catchPath));
461
- }
462
- return out;
395
+
396
+ // src/moduleInit.ts
397
+ function moduleInitStructure(options) {
398
+ const { name, file, range, effects } = options;
399
+ return {
400
+ identity: {
401
+ name,
402
+ kind: "module-init",
403
+ file,
404
+ range,
405
+ exportName: null,
406
+ exportPath: null
407
+ },
408
+ boundaryBinding: null,
409
+ parameters: [],
410
+ branches: [
411
+ {
412
+ conditions: [],
413
+ terminal: {
414
+ kind: "void",
415
+ statusCode: null,
416
+ body: null,
417
+ exceptionType: null,
418
+ message: null,
419
+ component: null,
420
+ renderTree: null,
421
+ delegateTarget: null,
422
+ emitEvent: null,
423
+ location: range
424
+ },
425
+ effects: [],
426
+ extraEffects: effects,
427
+ location: range,
428
+ isDefault: true
429
+ }
430
+ ],
431
+ unmatchedReturns: 0,
432
+ dependencyCalls: [],
433
+ declaredContract: null
434
+ };
463
435
  }
464
- function stepExit(stmt, path4, ctx) {
465
- const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
466
- if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
467
- collector.push(path4);
436
+
437
+ // src/packHelpers.ts
438
+ function httpRouteDiscovery(opts) {
439
+ const kind = opts.kind ?? "handler";
440
+ const calls = opts.importNames.map((importName) => ({
441
+ kind,
442
+ match: {
443
+ type: "registrationCall",
444
+ importModule: opts.importModule,
445
+ importName,
446
+ registrationChain: [...opts.methods]
447
+ },
448
+ bindingExtraction: {
449
+ // `.all` registers every method, so it records "*", which the pairing
450
+ // engine treats as agreeing with any method at all.
451
+ method: {
452
+ type: "fromRegistration",
453
+ position: "methodName",
454
+ nameMap: { all: "*" }
455
+ },
456
+ path: { type: "fromArgument", position: 0 }
457
+ },
458
+ ...opts.mount !== void 0 ? { mount: opts.mount } : {},
459
+ requiresImport: [opts.importModule]
460
+ }));
461
+ if (opts.importNames.length === 0) {
462
+ return calls;
468
463
  }
469
- return [];
464
+ return [
465
+ ...calls,
466
+ // Routes registered in a loop over an array of specs, a shape
467
+ // registration-call discovery cannot see. The receiver comes from
468
+ // the same import declaration as the calls above, so a loop that
469
+ // never touches this library's routable is left alone.
470
+ {
471
+ kind,
472
+ match: {
473
+ type: "registrationLoop",
474
+ elementShape: LOOP_ELEMENT_SHAPE,
475
+ receiver: {
476
+ importModule: opts.importModule,
477
+ importNames: [...opts.importNames]
478
+ }
479
+ },
480
+ requiresImport: [opts.importModule]
481
+ }
482
+ ];
470
483
  }
471
- function stepOpaque(_stmt, path4, _ctx) {
472
- return [path4];
484
+ function wrapperDiscovery(opts) {
485
+ return opts.importNames.flatMap(
486
+ (importName) => opts.wraps.map((wraps) => ({
487
+ kind: opts.kind ?? "middleware",
488
+ match: {
489
+ type: "registrationCall",
490
+ importModule: opts.importModule,
491
+ importName,
492
+ registrationChain: []
493
+ },
494
+ wraps: { ...wraps },
495
+ requiresImport: [opts.importModule]
496
+ }))
497
+ );
473
498
  }
474
- var stepHandlers = {
475
- if: stepIf,
476
- switch: stepSwitch,
477
- loop: stepLoop,
478
- try: stepTry,
479
- exit: stepExit,
480
- opaque: stepOpaque
499
+ var LOOP_ELEMENT_SHAPE = {
500
+ methodKey: "method",
501
+ pathKey: "path",
502
+ handlerKey: "handler"
481
503
  };
482
- function stepCallback(ctx, body, path4) {
483
- const exits = [];
484
- ctx.state.callbackExits.push(exits);
485
- let fellThrough;
486
- try {
487
- fellThrough = enumerate(ctx, body, path4);
488
- } finally {
489
- ctx.state.callbackExits.pop();
490
- }
491
- const out = [...fellThrough, ...exits];
492
- return out.length === 0 ? [path4] : out;
493
- }
494
- function stepStatement(ctx, stmt, path4) {
495
- let paths = [path4];
496
- for (const body of stmt.callbacks ?? []) {
497
- paths = paths.flatMap((each) => stepCallback(ctx, body, each));
498
- }
499
- const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
500
- for (const each of withRejoined(paths)) {
501
- for (const terminal of ownTerminals) {
502
- recordTerminal(ctx, terminal, stmt, each);
504
+ function registrationHelperDiscovery(helpers, kind = "handler") {
505
+ return helpers.map((helper) => ({
506
+ kind,
507
+ match: {
508
+ type: "registrationTemplate",
509
+ helperName: helper.helperName,
510
+ ...helper.importModule !== void 0 ? { importModule: helper.importModule } : {},
511
+ ...helper.subject !== void 0 ? { subject: helper.subject } : {},
512
+ registrations: helper.registrations.map((one) => ({ ...one }))
503
513
  }
504
- }
505
- const handler = stepHandlers[stmt.kind];
506
- return paths.flatMap(
507
- (each) => handler(
508
- stmt,
509
- each,
510
- ctx
514
+ }));
515
+ }
516
+ function routeHelperIndex(opts) {
517
+ const methods = new Set(
518
+ opts.methods.map(
519
+ (method) => method.startsWith(".") ? method.slice(1) : method
511
520
  )
512
521
  );
522
+ return {
523
+ find: { by: "subject" },
524
+ declare: (helpers) => ({
525
+ discovery: registrationHelperDiscovery(
526
+ helpers.flatMap((helper) => routesRegisteredBy(helper, methods, opts)),
527
+ opts.kind ?? "handler"
528
+ )
529
+ })
530
+ };
513
531
  }
514
- function soleDisagreement(left, right) {
515
- if (left.length !== right.length) {
516
- return null;
517
- }
518
- let found = null;
519
- for (let at = 0; at < left.length; at++) {
520
- const one = left[at];
521
- const other = right[at];
522
- if (one === other) {
532
+ function routesRegisteredBy(helper, methods, opts) {
533
+ const registrations = [];
534
+ let subjectArgument;
535
+ for (const sink of helper.sinks) {
536
+ const registration = routeRegisteredBy(helper, sink, methods);
537
+ if (registration === null) {
523
538
  continue;
524
539
  }
525
- if (found !== null) {
526
- return null;
540
+ registrations.push(registration);
541
+ if (sink.receiver.as === "parameter") {
542
+ subjectArgument = sink.receiver.position;
527
543
  }
528
- if (one === void 0 || other === void 0 || one.branchStmt === null || one.branchStmt !== other.branchStmt || one.info.polarity === other.info.polarity) {
529
- return null;
544
+ }
545
+ if (registrations.length === 0 || subjectArgument === void 0) {
546
+ return [];
547
+ }
548
+ return [
549
+ {
550
+ helperName: helper.name,
551
+ importModule: helper.file,
552
+ subject: {
553
+ argument: subjectArgument,
554
+ importModule: opts.importModule,
555
+ importNames: [...opts.importNames]
556
+ },
557
+ registrations
530
558
  }
531
- found = at;
559
+ ];
560
+ }
561
+ function routeRegisteredBy(helper, sink, methods) {
562
+ if (sink.method === null || !methods.has(sink.method)) {
563
+ return null;
532
564
  }
533
- return found;
565
+ const { receiver } = sink;
566
+ if (receiver.as !== "parameter" || receiver.property !== void 0 || !helper.subjectParameters.includes(receiver.position)) {
567
+ return null;
568
+ }
569
+ const pathTemplate = sink.arguments[0];
570
+ const handler = sink.arguments[sink.arguments.length - 1];
571
+ if (sink.arguments.length < 2 || pathTemplate === void 0 || pathTemplate.as !== "text" || handler === void 0) {
572
+ return null;
573
+ }
574
+ const handlerArg = handlerSlot(handler);
575
+ return handlerArg === null ? null : {
576
+ // `.all` registers every method, which the pairing engine
577
+ // spells as a wildcard.
578
+ method: sink.method === "all" ? "*" : sink.method.toUpperCase(),
579
+ pathTemplate: pathTemplate.text,
580
+ handlerArg
581
+ };
534
582
  }
535
- function mergeRejoined(paths) {
536
- let current = paths;
537
- for (let round = 0; round < current.length; round++) {
538
- const out = [];
539
- const taken = /* @__PURE__ */ new Set();
540
- let mergedAny = false;
541
- for (let i = 0; i < current.length; i++) {
542
- if (taken.has(i)) {
543
- continue;
544
- }
545
- let combined = current[i];
546
- for (let j = i + 1; j < current.length; j++) {
547
- if (taken.has(j)) {
548
- continue;
549
- }
550
- const at = soleDisagreement(combined, current[j]);
551
- if (at === null) {
552
- continue;
553
- }
554
- combined = [...combined.slice(0, at), ...combined.slice(at + 1)];
555
- taken.add(j);
556
- mergedAny = true;
557
- break;
558
- }
559
- out.push(combined);
560
- }
561
- current = out;
562
- if (!mergedAny) {
563
- break;
564
- }
583
+ function handlerSlot(value) {
584
+ if (value.as !== "parameter") {
585
+ return null;
565
586
  }
566
- return current;
567
- }
568
- function withRejoined(paths) {
569
- const merged = mergeRejoined(paths);
570
- return merged.length === paths.length ? paths : [...merged, ...paths];
587
+ return value.property === void 0 ? `{${value.position}}` : `{${value.position}}.${value.property}`;
571
588
  }
572
- function enumerate(ctx, stmts, prefix) {
573
- let frontiers = [[...prefix]];
574
- for (const stmt of stmts) {
575
- const nextFrontiers = [];
576
- for (const path4 of mergeRejoined(frontiers)) {
577
- nextFrontiers.push(...stepStatement(ctx, stmt, path4));
578
- }
579
- frontiers = nextFrontiers;
580
- if (frontiers.length > MAX_PATHS) {
581
- throw new PathBudgetExceeded(
582
- `path budget exceeded, more than ${MAX_PATHS} paths`
583
- );
584
- }
585
- if (frontiers.length === 0) {
586
- break;
587
- }
589
+ function unwrapJsonStringify(body) {
590
+ if (body === null || typeof body !== "object") {
591
+ return body;
588
592
  }
589
- return frontiers;
590
- }
591
- function buildParentMap(statements) {
592
- const parentOf = /* @__PURE__ */ new Map();
593
- const visit = (stmt, parent) => {
594
- parentOf.set(stmt, parent);
595
- for (const child of childrenOf(stmt)) {
596
- visit(child, stmt);
597
- }
598
- };
599
- for (const stmt of statements) {
600
- visit(stmt, null);
593
+ const candidate = body;
594
+ if (candidate.kind !== "call" || candidate.callee !== "JSON.stringify") {
595
+ return body;
601
596
  }
602
- return parentOf;
597
+ const inner = candidate.args?.[0];
598
+ return inner ?? body;
603
599
  }
604
- function enumerateStructuredPaths(input) {
605
- const parentOf = buildParentMap(input.statements);
606
- const isAncestorOrSelf = (a, b) => {
607
- let current = b;
608
- while (current !== null) {
609
- if (current === a) {
610
- return true;
611
- }
612
- current = parentOf.get(current) ?? null;
613
- }
614
- return false;
615
- };
616
- const state = {
617
- byTerminal: /* @__PURE__ */ new Map(),
618
- fallthrough: [],
619
- pathCount: 0,
620
- callbackExits: []
600
+
601
+ // src/packOptions.ts
602
+ var import_zod = require("zod");
603
+ var storageSystemOption = import_zod.z.enum([
604
+ "postgresql",
605
+ "mysql",
606
+ "sqlite"
607
+ ]);
608
+ var scopeOption = import_zod.z.string();
609
+ var configuredCallOption = import_zod.z.object({
610
+ /** Module that declares the receiver's type. */
611
+ module: import_zod.z.string(),
612
+ /** Type name of the receiver, as exported from that module. */
613
+ receiver: import_zod.z.string(),
614
+ /** Method that performs the send. */
615
+ method: import_zod.z.string(),
616
+ /** Argument index carrying the subject. */
617
+ subjectArg: import_zod.z.number(),
618
+ /**
619
+ * Which argument the message body is. Left out when the method has no
620
+ * single body argument, as a batch method taking a list of entries
621
+ * does, and then no body is reported.
622
+ */
623
+ bodyArg: import_zod.z.number().optional()
624
+ }).strict();
625
+
626
+ // src/paths/enumeratePaths.ts
627
+ var import_behavioral_ir3 = require("@suss/behavioral-ir");
628
+ var MAX_PATHS = 256;
629
+ var UnmodeledFlow = class extends Error {
630
+ };
631
+ var PathBudgetExceeded = class extends Error {
632
+ };
633
+ function branchCond(branchStmt, condition, polarity, oppositeExit) {
634
+ return {
635
+ info: {
636
+ sourceText: condition.sourceText,
637
+ polarity,
638
+ source: "explicit",
639
+ expression: condition.expression
640
+ },
641
+ branchStmt,
642
+ oppositeExit
621
643
  };
622
- const ctx = {
623
- terminalsByStmt: input.terminalsByStmt,
624
- state,
625
- isAncestorOrSelf
644
+ }
645
+ function fixedCond(condition, polarity, source) {
646
+ return {
647
+ info: {
648
+ sourceText: condition.sourceText,
649
+ polarity,
650
+ source,
651
+ expression: condition.expression
652
+ },
653
+ branchStmt: null,
654
+ oppositeExit: null
626
655
  };
627
- const continuations = withRejoined(enumerate(ctx, input.statements, []));
628
- for (const path4 of continuations) {
629
- chargeBudget(state);
630
- state.fallthrough.push(classify(path4, null, isAncestorOrSelf));
631
- }
632
- return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
633
656
  }
634
- function enumerateOrDegrade(input, terminals) {
635
- try {
636
- return { ...enumerateStructuredPaths(input), degraded: null };
637
- } catch (error) {
638
- const reason = reasonToDegrade(error);
639
- if (reason === null) {
640
- throw error;
657
+ var loopIterationCond = (headerText) => fixedCond(
658
+ { sourceText: `some iteration of: ${headerText}`, expression: null },
659
+ "positive",
660
+ "explicit"
661
+ );
662
+ var loopCompletedCond = (headerText, exit) => fixedCond(
663
+ {
664
+ sourceText: `loop exited via ${exit ?? "return"}: ${headerText}`,
665
+ expression: null
666
+ },
667
+ "negative",
668
+ exit === "throw" ? "earlyThrow" : "earlyReturn"
669
+ );
670
+ var catchEntryCond = () => fixedCond(
671
+ { sourceText: import_behavioral_ir3.CATCH_ENTRY_TEXT, expression: null },
672
+ "positive",
673
+ "catchBlock"
674
+ );
675
+ function classify(path4, home, isAncestorOrSelf) {
676
+ return path4.map((cond) => {
677
+ if (cond.branchStmt === null) {
678
+ return cond.info;
641
679
  }
642
- const marker = {
643
- sourceText: `unmodeled control flow (${reason})`,
644
- polarity: "positive",
645
- source: "explicit",
646
- expression: null
647
- };
648
- const byTerminal = /* @__PURE__ */ new Map();
649
- for (const terminal of terminals) {
650
- byTerminal.set(terminal, [[marker]]);
680
+ const encloses = home !== null && isAncestorOrSelf(cond.branchStmt, home);
681
+ if (encloses || cond.info.polarity === "positive" || cond.oppositeExit === null) {
682
+ return { ...cond.info, source: "explicit" };
651
683
  }
652
- return { byTerminal, fallthrough: [[marker]], degraded: reason };
653
- }
684
+ const source = cond.oppositeExit === "throw" ? "earlyThrow" : "earlyReturn";
685
+ return { ...cond.info, source };
686
+ });
654
687
  }
655
- function reasonToDegrade(error) {
656
- if (error instanceof PathBudgetExceeded) {
657
- return error.message === "" ? "path budget exceeded" : error.message;
688
+ function childrenOf(stmt) {
689
+ return [...ownBlocksOf(stmt), ...(stmt.callbacks ?? []).flat()];
690
+ }
691
+ function ownBlocksOf(stmt) {
692
+ if (stmt.kind === "if") {
693
+ return stmt.elseBody === null ? stmt.thenBody : [...stmt.thenBody, ...stmt.elseBody];
658
694
  }
659
- if (error instanceof UnmodeledFlow) {
660
- return error.message;
695
+ if (stmt.kind === "switch") {
696
+ return stmt.groups.flatMap((g) => g.body);
661
697
  }
662
- return null;
663
- }
664
-
665
- // src/composeWrappers.ts
666
- var BUDGET_GAP = {
667
- type: "unreadOutcome",
668
- conditions: [],
669
- consequence: "unknown",
670
- description: `Composing the wrappers registered around this unit would have gone past the path budget of ${MAX_PATHS}, so what each of them produces is reported beside this unit's own outcomes rather than under the conditions that reach it`
671
- };
672
- function composeWrappers(summaries, options = {}) {
673
- const byKey = /* @__PURE__ */ new Map();
674
- for (const summary of summaries) {
675
- const key = summaryKey(summary.location.file, summary.identity.name);
676
- const sharing = byKey.get(key);
677
- if (sharing === void 0) {
678
- byKey.set(key, [summary]);
679
- continue;
680
- }
681
- sharing.push(summary);
698
+ if (stmt.kind === "loop") {
699
+ return stmt.body;
682
700
  }
683
- const keepGaps = options.gapHandling !== "silent";
684
- return summaries.map((summary) => composeOne(summary, byKey, keepGaps));
701
+ if (stmt.kind === "try") {
702
+ return [
703
+ ...stmt.tryBody,
704
+ ...stmt.catchBody ?? [],
705
+ ...stmt.finallyBody ?? []
706
+ ];
707
+ }
708
+ return [];
685
709
  }
686
- function summaryKey(file, name) {
687
- return `${file}::${name}`;
710
+ function hasStrayBreak(nodes) {
711
+ return nodes.some((node) => {
712
+ if (node.kind === "exit" && node.exit === "break") {
713
+ return true;
714
+ }
715
+ if (node.kind === "switch" || node.kind === "loop") {
716
+ return false;
717
+ }
718
+ return hasStrayBreak(ownBlocksOf(node));
719
+ });
688
720
  }
689
- function summaryOf(reference, byKey) {
690
- const sharing = byKey.get(summaryKey(reference.file, reference.name)) ?? [];
691
- if (reference.line === void 0) {
692
- return sharing[0];
721
+ function exitKindOfList(stmts) {
722
+ let sawReturn = false;
723
+ for (const stmt of stmts) {
724
+ if (stmt.exitKind === "throw") {
725
+ return "throw";
726
+ }
727
+ if (stmt.exitKind === "return") {
728
+ sawReturn = true;
729
+ }
693
730
  }
694
- return sharing.find((one) => one.location.range.start === reference.line) ?? sharing[0];
731
+ return sawReturn ? "return" : null;
695
732
  }
696
- function composeOne(summary, byKey, keepGaps) {
697
- const recorded = (0, import_behavioral_ir3.readWrapperMetadata)(summary);
698
- const applied = recorded?.applied ?? [];
699
- const covering = applied.filter(
700
- (reference) => coversUnit(reference, summary)
701
- );
702
- const narrowed = covering.length === applied.length ? summary : {
703
- ...summary,
704
- metadata: (0, import_behavioral_ir3.withWrapperMetadata)(summary.metadata, {
705
- applied: covering
706
- })
707
- };
708
- const wrappers = covering.flatMap((reference) => {
709
- const found = summaryOf(reference, byKey);
710
- return found === void 0 ? [] : [{ reference, summary: found }];
711
- });
712
- if (wrappers.length === 0) {
713
- return narrowed;
714
- }
715
- let composition = {
716
- transitions: summary.transitions,
717
- degraded: false
718
- };
719
- for (const wrapper of [...wrappers].reverse()) {
720
- if (wrapper.reference.onThrow === true) {
721
- continue;
722
- }
723
- composition = merge(composition, applyWrapper(wrapper, composition));
724
- }
725
- for (const wrapper of wrappers) {
726
- if (wrapper.reference.onThrow !== true) {
727
- continue;
728
- }
729
- composition = merge(composition, applyThrowWrapper(wrapper, composition));
730
- }
731
- const transitions = composition.transitions === summary.transitions ? summary.transitions : withDistinctIds(composition.transitions);
732
- if (!keepGaps) {
733
- return { ...narrowed, transitions };
734
- }
735
- const gaps = withContractStatusGaps(summary, transitions, wrappers);
736
- if (transitions === summary.transitions && gaps === summary.gaps) {
737
- return narrowed;
738
- }
739
- return {
740
- ...narrowed,
741
- transitions,
742
- gaps: composition.degraded ? [...gaps, BUDGET_GAP] : gaps
743
- };
744
- }
745
- function withContractStatusGaps(summary, composed, wrappers) {
746
- const contract = (0, import_behavioral_ir3.readHttpMetadata)(summary)?.declaredContract;
747
- const binding = summary.identity.boundaryBinding;
748
- if (contract === void 0 || binding !== null && !(0, import_behavioral_ir3.exchangesHttpResponses)(binding)) {
749
- return summary.gaps;
733
+ function chargeBudget(state) {
734
+ state.pathCount++;
735
+ if (state.pathCount > MAX_PATHS) {
736
+ throw new PathBudgetExceeded(
737
+ `path budget exceeded, more than ${MAX_PATHS} paths`
738
+ );
750
739
  }
751
- const declared = {
752
- framework: contract.framework ?? "declared",
753
- responses: contract.responses
754
- };
755
- const stale = new Set(
756
- contractStatusGaps(declared, summary.transitions).map(gapKey)
757
- );
758
- const onThrow = wrappers.filter((wrapper) => wrapper.reference.onThrow === true).flatMap(
759
- (wrapper) => attribute(wrapper.summary.transitions, wrapper.reference)
760
- );
761
- const gaps = [
762
- ...summary.gaps.filter((gap) => !stale.has(gapKey(gap))),
763
- ...contractStatusGaps(declared, [...composed, ...onThrow])
764
- ];
765
- const unchanged = gaps.length === summary.gaps.length && gaps.every((gap, i) => gapKey(gap) === gapKey(summary.gaps[i]));
766
- return unchanged ? summary.gaps : gaps;
767
740
  }
768
- function gapKey(gap) {
769
- return `${gap.type}|${gap.description}`;
770
- }
771
- function merge(before, step) {
772
- return {
773
- transitions: step.transitions,
774
- degraded: before.degraded || step.degraded
775
- };
741
+ function recordTerminal(ctx, terminal, home, path4) {
742
+ chargeBudget(ctx.state);
743
+ const info = classify(path4, home, ctx.isAncestorOrSelf);
744
+ const existing = ctx.state.byTerminal.get(terminal) ?? [];
745
+ existing.push(info);
746
+ ctx.state.byTerminal.set(terminal, existing);
776
747
  }
777
- function coversUnit(reference, summary) {
778
- if (reference.scope === void 0) {
748
+ function containsTerminal(ctx, stmt) {
749
+ if ((ctx.terminalsByStmt.get(stmt)?.length ?? 0) > 0) {
779
750
  return true;
780
751
  }
781
- const binding = summary.identity.boundaryBinding;
782
- return binding !== null && (0, import_behavioral_ir3.withinScope)(binding, reference.scope);
783
- }
784
- function applyWrapper(wrapper, inner) {
785
- const shortCircuits = attribute(
786
- wrapper.summary.transitions.filter((t) => t.output.type !== "delegate"),
787
- wrapper.reference
788
- );
789
- const passThroughs = withoutCoveredPassThroughs(
790
- wrapper.summary.transitions.filter((t) => t.output.type === "delegate")
791
- );
792
- if (passThroughs.length === 0 || shortCircuits.length + passThroughs.length * inner.transitions.length > MAX_PATHS) {
793
- return {
794
- transitions: [...shortCircuits, ...inner.transitions],
795
- degraded: passThroughs.length > 0
796
- };
797
- }
798
- const continued = passThroughs.flatMap(
799
- (passThrough) => inner.transitions.map((transition) => splice(passThrough, transition))
800
- );
801
- return { transitions: [...shortCircuits, ...continued], degraded: false };
752
+ return childrenOf(stmt).some((child) => containsTerminal(ctx, child));
802
753
  }
803
- function withoutCoveredPassThroughs(passThroughs) {
804
- return passThroughs.filter(
805
- (candidate, i) => !passThroughs.some(
806
- (other, j) => j !== i && covers(other, candidate) && (j < i || !covers(candidate, other))
754
+ function stepIf(stmt, path4, ctx) {
755
+ const thenExit = exitKindOfList(stmt.thenBody);
756
+ const elseExit = stmt.elseBody === null ? null : exitKindOfList(stmt.elseBody);
757
+ const thenPath = [
758
+ ...path4,
759
+ branchCond(stmt, stmt.condition, "positive", elseExit)
760
+ ];
761
+ const elsePath = [
762
+ ...path4,
763
+ branchCond(
764
+ stmt,
765
+ stmt.condition,
766
+ "negative",
767
+ stmt.elseBody === null ? thenExit : null
807
768
  )
808
- );
809
- }
810
- function covers(wider, narrower) {
811
- if (JSON.stringify(wider.effects) !== JSON.stringify(narrower.effects)) {
812
- return false;
769
+ ];
770
+ const out = [];
771
+ out.push(...enumerate(ctx, stmt.thenBody, thenPath));
772
+ if (stmt.elseBody !== null) {
773
+ out.push(...enumerate(ctx, stmt.elseBody, elsePath));
774
+ } else {
775
+ out.push(elsePath);
813
776
  }
814
- const required = new Set(narrower.conditions.map((c) => JSON.stringify(c)));
815
- return wider.conditions.every((c) => required.has(JSON.stringify(c)));
777
+ return out;
816
778
  }
817
- function applyThrowWrapper(wrapper, inner) {
818
- const thrown = inner.transitions.filter((t) => t.output.type === "throw");
819
- if (thrown.length === 0) {
820
- return inner;
779
+ function stepSwitch(stmt, path4, ctx) {
780
+ const out = [];
781
+ const negations = [];
782
+ let defaultRanBody = false;
783
+ for (let i = 0; i < stmt.groups.length; i++) {
784
+ const group = stmt.groups[i];
785
+ const fallthroughSafe = i === stmt.groups.length - 1;
786
+ if (hasStrayBreak(group.body)) {
787
+ throw new UnmodeledFlow("non-trailing break in switch clause");
788
+ }
789
+ const groupPath = group.condition === null ? [...path4, ...negations] : [...path4, fixedCond(group.condition, "positive", "explicit")];
790
+ const conts = enumerate(ctx, group.body, groupPath);
791
+ if (conts.length > 0) {
792
+ if (!group.hasTrailingBreak && !fallthroughSafe) {
793
+ throw new UnmodeledFlow("fallthrough into a non-empty switch clause");
794
+ }
795
+ out.push(...conts);
796
+ }
797
+ if (group.condition === null) {
798
+ defaultRanBody = true;
799
+ } else {
800
+ const exit = exitKindOfList(group.body);
801
+ const negationSource = exit === "throw" ? "earlyThrow" : exit === "return" ? "earlyReturn" : "explicit";
802
+ negations.push(fixedCond(group.condition, "negative", negationSource));
803
+ }
821
804
  }
822
- const rest = inner.transitions.filter((t) => t.output.type !== "throw");
823
- const handled = wrapper.summary.transitions;
824
- if (rest.length + thrown.length * handled.length > MAX_PATHS) {
825
- return {
826
- transitions: [
827
- ...inner.transitions,
828
- ...attribute(handled, wrapper.reference)
829
- ],
830
- degraded: true
831
- };
805
+ if (!defaultRanBody) {
806
+ out.push([...path4, ...negations]);
832
807
  }
833
- const composed = thrown.flatMap(
834
- (transition) => attribute(
835
- handled.map((handler) => splice(transition, handler)),
836
- wrapper.reference
837
- )
838
- );
839
- return { transitions: [...rest, ...composed], degraded: false };
840
- }
841
- function splice(outer, inner) {
842
- return {
843
- ...inner,
844
- id: `${inner.id}:via:${outer.id}`,
845
- conditions: [...outer.conditions, ...inner.conditions],
846
- effects: [...outer.effects, ...inner.effects],
847
- isDefault: outer.isDefault && inner.isDefault
848
- };
849
- }
850
- function attribute(transitions, reference) {
851
- return transitions.map((transition) => ({
852
- ...transition,
853
- metadata: (0, import_behavioral_ir3.withWrapperMetadata)(transition.metadata, { from: reference })
854
- }));
855
- }
856
- function withDistinctIds(transitions) {
857
- const seen = /* @__PURE__ */ new Map();
858
- return transitions.map((transition) => {
859
- const taken = seen.get(transition.id);
860
- seen.set(transition.id, (taken ?? 0) + 1);
861
- return taken === void 0 ? transition : { ...transition, id: `${transition.id}#${taken + 1}` };
862
- });
808
+ return out;
863
809
  }
864
-
865
- // src/effectGuards.ts
866
- var keyOf = (condition) => `${condition.polarity}:${condition.sourceText}`;
867
- var opposite = (condition) => `${condition.polarity === "positive" ? "negative" : "positive"}:${condition.sourceText}`;
868
- function guardsHoldOn(preconditions, conditions) {
869
- if (preconditions === void 0 || preconditions.length === 0) {
870
- return true;
810
+ function stepLoop(stmt, path4, ctx) {
811
+ const bodyExit = exitKindOfList(stmt.body);
812
+ if (containsTerminal(ctx, stmt)) {
813
+ enumerate(ctx, stmt.body, [
814
+ ...path4,
815
+ loopIterationCond(stmt.condition.sourceText)
816
+ ]);
871
817
  }
872
- const onBranch = new Set(conditions.map(keyOf));
873
- return preconditions.every(
874
- (precondition) => !onBranch.has(opposite(precondition))
875
- );
876
- }
877
- function runsBefore(effectLine, terminalEndLine) {
878
- return effectLine <= terminalEndLine;
818
+ if (bodyExit !== null) {
819
+ return [[...path4, loopCompletedCond(stmt.condition.sourceText, bodyExit)]];
820
+ }
821
+ return [path4];
879
822
  }
880
-
881
- // src/moduleImports.ts
882
- function stampModuleImports(summaries, importsOf) {
883
- const byFile = /* @__PURE__ */ new Map();
884
- for (const summary of summaries) {
885
- const file = summary.location.file;
886
- if (!byFile.has(file)) {
887
- const found = importsOf(file);
888
- byFile.set(
889
- file,
890
- found === void 0 ? void 0 : [...new Set(found)].sort()
891
- );
892
- }
893
- const imports = byFile.get(file);
894
- if (imports === void 0) {
895
- continue;
823
+ function stepTry(stmt, path4, ctx) {
824
+ if (stmt.finallyBody !== null) {
825
+ const finallyHasTerminal = stmt.finallyBody.some(
826
+ (s) => containsTerminal(ctx, s)
827
+ );
828
+ if (exitKindOfList(stmt.finallyBody) !== null || finallyHasTerminal) {
829
+ throw new UnmodeledFlow("finally block with exits or terminals");
896
830
  }
897
- summary.metadata = {
898
- ...summary.metadata ?? {},
899
- moduleImports: imports
900
- };
901
831
  }
832
+ const out = [];
833
+ out.push(...enumerate(ctx, stmt.tryBody, path4));
834
+ if (stmt.catchBody !== null) {
835
+ const catchPath = [...path4, catchEntryCond()];
836
+ out.push(...enumerate(ctx, stmt.catchBody, catchPath));
837
+ }
838
+ return out;
902
839
  }
903
-
904
- // src/moduleInit.ts
905
- function moduleInitStructure(options) {
906
- const { name, file, range, effects } = options;
907
- return {
908
- identity: {
909
- name,
910
- kind: "module-init",
911
- file,
912
- range,
913
- exportName: null,
914
- exportPath: null
915
- },
916
- boundaryBinding: null,
917
- parameters: [],
918
- branches: [
919
- {
920
- conditions: [],
921
- terminal: {
922
- kind: "void",
923
- statusCode: null,
924
- body: null,
925
- exceptionType: null,
926
- message: null,
927
- component: null,
928
- renderTree: null,
929
- delegateTarget: null,
930
- emitEvent: null,
931
- location: range
932
- },
933
- effects: [],
934
- extraEffects: effects,
935
- location: range,
936
- isDefault: true
937
- }
938
- ],
939
- unmatchedReturns: 0,
940
- dependencyCalls: [],
941
- declaredContract: null
942
- };
943
- }
944
-
945
- // src/packHelpers.ts
946
- function httpRouteDiscovery(opts) {
947
- const kind = opts.kind ?? "handler";
948
- const calls = opts.importNames.map((importName) => ({
949
- kind,
950
- match: {
951
- type: "registrationCall",
952
- importModule: opts.importModule,
953
- importName,
954
- registrationChain: [...opts.methods]
955
- },
956
- bindingExtraction: {
957
- // `.all` registers every method, so it records "*", which the pairing
958
- // engine treats as agreeing with any method at all.
959
- method: {
960
- type: "fromRegistration",
961
- position: "methodName",
962
- nameMap: { all: "*" }
963
- },
964
- path: { type: "fromArgument", position: 0 }
965
- },
966
- ...opts.mount !== void 0 ? { mount: opts.mount } : {},
967
- requiresImport: [opts.importModule]
968
- }));
969
- if (opts.importNames.length === 0) {
970
- return calls;
840
+ function stepExit(stmt, path4, ctx) {
841
+ const collector = ctx.state.callbackExits[ctx.state.callbackExits.length - 1];
842
+ if (collector !== void 0 && (stmt.exit === "return" || stmt.exit === "throw")) {
843
+ collector.push(path4);
971
844
  }
972
- return [
973
- ...calls,
974
- // Routes registered in a loop over an array of specs, a shape
975
- // registration-call discovery cannot see. The receiver comes from
976
- // the same import declaration as the calls above, so a loop that
977
- // never touches this library's routable is left alone.
978
- {
979
- kind,
980
- match: {
981
- type: "registrationLoop",
982
- elementShape: LOOP_ELEMENT_SHAPE,
983
- receiver: {
984
- importModule: opts.importModule,
985
- importNames: [...opts.importNames]
986
- }
987
- },
988
- requiresImport: [opts.importModule]
989
- }
990
- ];
845
+ return [];
991
846
  }
992
- function wrapperDiscovery(opts) {
993
- return opts.importNames.flatMap(
994
- (importName) => opts.wraps.map((wraps) => ({
995
- kind: opts.kind ?? "middleware",
996
- match: {
997
- type: "registrationCall",
998
- importModule: opts.importModule,
999
- importName,
1000
- registrationChain: []
1001
- },
1002
- wraps: { ...wraps },
1003
- requiresImport: [opts.importModule]
1004
- }))
1005
- );
847
+ function stepOpaque(_stmt, path4, _ctx) {
848
+ return [path4];
1006
849
  }
1007
- var LOOP_ELEMENT_SHAPE = {
1008
- methodKey: "method",
1009
- pathKey: "path",
1010
- handlerKey: "handler"
1011
- };
1012
- function registrationHelperDiscovery(helpers, kind = "handler") {
1013
- return helpers.map((helper) => ({
1014
- kind,
1015
- match: {
1016
- type: "registrationTemplate",
1017
- helperName: helper.helperName,
1018
- ...helper.importModule !== void 0 ? { importModule: helper.importModule } : {},
1019
- ...helper.subject !== void 0 ? { subject: helper.subject } : {},
1020
- registrations: helper.registrations.map((one) => ({ ...one }))
1021
- }
1022
- }));
850
+ var stepHandlers = {
851
+ if: stepIf,
852
+ switch: stepSwitch,
853
+ loop: stepLoop,
854
+ try: stepTry,
855
+ exit: stepExit,
856
+ opaque: stepOpaque
857
+ };
858
+ function stepCallback(ctx, body, path4) {
859
+ const exits = [];
860
+ ctx.state.callbackExits.push(exits);
861
+ let fellThrough;
862
+ try {
863
+ fellThrough = enumerate(ctx, body, path4);
864
+ } finally {
865
+ ctx.state.callbackExits.pop();
866
+ }
867
+ const out = [...fellThrough, ...exits];
868
+ return out.length === 0 ? [path4] : out;
1023
869
  }
1024
- function routeHelperIndex(opts) {
1025
- const methods = new Set(
1026
- opts.methods.map(
1027
- (method) => method.startsWith(".") ? method.slice(1) : method
870
+ function stepStatement(ctx, stmt, path4) {
871
+ let paths = [path4];
872
+ for (const body of stmt.callbacks ?? []) {
873
+ paths = paths.flatMap((each) => stepCallback(ctx, body, each));
874
+ }
875
+ const ownTerminals = ctx.terminalsByStmt.get(stmt) ?? [];
876
+ for (const each of withRejoined(paths)) {
877
+ for (const terminal of ownTerminals) {
878
+ recordTerminal(ctx, terminal, stmt, each);
879
+ }
880
+ }
881
+ const handler = stepHandlers[stmt.kind];
882
+ return paths.flatMap(
883
+ (each) => handler(
884
+ stmt,
885
+ each,
886
+ ctx
1028
887
  )
1029
888
  );
1030
- return {
1031
- find: { by: "subject" },
1032
- declare: (helpers) => ({
1033
- discovery: registrationHelperDiscovery(
1034
- helpers.flatMap((helper) => routesRegisteredBy(helper, methods, opts)),
1035
- opts.kind ?? "handler"
1036
- )
1037
- })
1038
- };
1039
889
  }
1040
- function routesRegisteredBy(helper, methods, opts) {
1041
- const registrations = [];
1042
- let subjectArgument;
1043
- for (const sink of helper.sinks) {
1044
- const registration = routeRegisteredBy(helper, sink, methods);
1045
- if (registration === null) {
890
+ function soleDisagreement(left, right) {
891
+ if (left.length !== right.length) {
892
+ return null;
893
+ }
894
+ let found = null;
895
+ for (let at = 0; at < left.length; at++) {
896
+ const one = left[at];
897
+ const other = right[at];
898
+ if (one === other) {
1046
899
  continue;
1047
900
  }
1048
- registrations.push(registration);
1049
- if (sink.receiver.as === "parameter") {
1050
- subjectArgument = sink.receiver.position;
901
+ if (found !== null) {
902
+ return null;
1051
903
  }
1052
- }
1053
- if (registrations.length === 0 || subjectArgument === void 0) {
1054
- return [];
1055
- }
1056
- return [
1057
- {
1058
- helperName: helper.name,
1059
- importModule: helper.file,
1060
- subject: {
1061
- argument: subjectArgument,
1062
- importModule: opts.importModule,
1063
- importNames: [...opts.importNames]
1064
- },
1065
- registrations
904
+ if (one === void 0 || other === void 0 || one.branchStmt === null || one.branchStmt !== other.branchStmt || one.info.polarity === other.info.polarity) {
905
+ return null;
1066
906
  }
1067
- ];
907
+ found = at;
908
+ }
909
+ return found;
1068
910
  }
1069
- function routeRegisteredBy(helper, sink, methods) {
1070
- if (sink.method === null || !methods.has(sink.method)) {
1071
- return null;
911
+ function mergeRejoined(paths) {
912
+ let current = paths;
913
+ for (let round = 0; round < current.length; round++) {
914
+ const out = [];
915
+ const taken = /* @__PURE__ */ new Set();
916
+ let mergedAny = false;
917
+ for (let i = 0; i < current.length; i++) {
918
+ if (taken.has(i)) {
919
+ continue;
920
+ }
921
+ let combined = current[i];
922
+ for (let j = i + 1; j < current.length; j++) {
923
+ if (taken.has(j)) {
924
+ continue;
925
+ }
926
+ const at = soleDisagreement(combined, current[j]);
927
+ if (at === null) {
928
+ continue;
929
+ }
930
+ combined = [...combined.slice(0, at), ...combined.slice(at + 1)];
931
+ taken.add(j);
932
+ mergedAny = true;
933
+ break;
934
+ }
935
+ out.push(combined);
936
+ }
937
+ current = out;
938
+ if (!mergedAny) {
939
+ break;
940
+ }
1072
941
  }
1073
- const { receiver } = sink;
1074
- if (receiver.as !== "parameter" || receiver.property !== void 0 || !helper.subjectParameters.includes(receiver.position)) {
1075
- return null;
942
+ return current;
943
+ }
944
+ function withRejoined(paths) {
945
+ const merged = mergeRejoined(paths);
946
+ return merged.length === paths.length ? paths : [...merged, ...paths];
947
+ }
948
+ function enumerate(ctx, stmts, prefix) {
949
+ let frontiers = [[...prefix]];
950
+ for (const stmt of stmts) {
951
+ const nextFrontiers = [];
952
+ for (const path4 of mergeRejoined(frontiers)) {
953
+ nextFrontiers.push(...stepStatement(ctx, stmt, path4));
954
+ }
955
+ frontiers = nextFrontiers;
956
+ if (frontiers.length > MAX_PATHS) {
957
+ throw new PathBudgetExceeded(
958
+ `path budget exceeded, more than ${MAX_PATHS} paths`
959
+ );
960
+ }
961
+ if (frontiers.length === 0) {
962
+ break;
963
+ }
1076
964
  }
1077
- const pathTemplate = sink.arguments[0];
1078
- const handler = sink.arguments[sink.arguments.length - 1];
1079
- if (sink.arguments.length < 2 || pathTemplate === void 0 || pathTemplate.as !== "text" || handler === void 0) {
1080
- return null;
965
+ return frontiers;
966
+ }
967
+ function buildParentMap(statements) {
968
+ const parentOf = /* @__PURE__ */ new Map();
969
+ const visit = (stmt, parent) => {
970
+ parentOf.set(stmt, parent);
971
+ for (const child of childrenOf(stmt)) {
972
+ visit(child, stmt);
973
+ }
974
+ };
975
+ for (const stmt of statements) {
976
+ visit(stmt, null);
1081
977
  }
1082
- const handlerArg = handlerSlot(handler);
1083
- return handlerArg === null ? null : {
1084
- // `.all` registers every method, which the pairing engine
1085
- // spells as a wildcard.
1086
- method: sink.method === "all" ? "*" : sink.method.toUpperCase(),
1087
- pathTemplate: pathTemplate.text,
1088
- handlerArg
978
+ return parentOf;
979
+ }
980
+ function enumerateStructuredPaths(input) {
981
+ const parentOf = buildParentMap(input.statements);
982
+ const isAncestorOrSelf = (a, b) => {
983
+ let current = b;
984
+ while (current !== null) {
985
+ if (current === a) {
986
+ return true;
987
+ }
988
+ current = parentOf.get(current) ?? null;
989
+ }
990
+ return false;
991
+ };
992
+ const state = {
993
+ byTerminal: /* @__PURE__ */ new Map(),
994
+ fallthrough: [],
995
+ pathCount: 0,
996
+ callbackExits: []
997
+ };
998
+ const ctx = {
999
+ terminalsByStmt: input.terminalsByStmt,
1000
+ state,
1001
+ isAncestorOrSelf
1089
1002
  };
1003
+ const continuations = withRejoined(enumerate(ctx, input.statements, []));
1004
+ for (const path4 of continuations) {
1005
+ chargeBudget(state);
1006
+ state.fallthrough.push(classify(path4, null, isAncestorOrSelf));
1007
+ }
1008
+ return { byTerminal: state.byTerminal, fallthrough: state.fallthrough };
1090
1009
  }
1091
- function handlerSlot(value) {
1092
- if (value.as !== "parameter") {
1093
- return null;
1010
+ function enumerateOrDegrade(input, terminals) {
1011
+ try {
1012
+ return { ...enumerateStructuredPaths(input), degraded: null };
1013
+ } catch (error) {
1014
+ const reason = reasonToDegrade(error);
1015
+ if (reason === null) {
1016
+ throw error;
1017
+ }
1018
+ const marker = {
1019
+ sourceText: `unmodeled control flow (${reason})`,
1020
+ polarity: "positive",
1021
+ source: "explicit",
1022
+ expression: null
1023
+ };
1024
+ const byTerminal = /* @__PURE__ */ new Map();
1025
+ for (const terminal of terminals) {
1026
+ byTerminal.set(terminal, [[marker]]);
1027
+ }
1028
+ return { byTerminal, fallthrough: [[marker]], degraded: reason };
1094
1029
  }
1095
- return value.property === void 0 ? `{${value.position}}` : `{${value.position}}.${value.property}`;
1096
1030
  }
1097
- function unwrapJsonStringify(body) {
1098
- if (body === null || typeof body !== "object") {
1099
- return body;
1031
+ function reasonToDegrade(error) {
1032
+ if (error instanceof PathBudgetExceeded) {
1033
+ return error.message === "" ? "path budget exceeded" : error.message;
1100
1034
  }
1101
- const candidate = body;
1102
- if (candidate.kind !== "call" || candidate.callee !== "JSON.stringify") {
1103
- return body;
1035
+ if (error instanceof UnmodeledFlow) {
1036
+ return error.message;
1104
1037
  }
1105
- const inner = candidate.args?.[0];
1106
- return inner ?? body;
1038
+ return null;
1107
1039
  }
1108
1040
 
1109
- // src/packOptions.ts
1110
- var import_zod = require("zod");
1111
- var storageSystemOption = import_zod.z.enum([
1112
- "postgresql",
1113
- "mysql",
1114
- "sqlite"
1115
- ]);
1116
- var scopeOption = import_zod.z.string();
1117
- var configuredCallOption = import_zod.z.object({
1118
- /** Module that declares the receiver's type. */
1119
- module: import_zod.z.string(),
1120
- /** Type name of the receiver, as exported from that module. */
1121
- receiver: import_zod.z.string(),
1122
- /** Method that performs the send. */
1123
- method: import_zod.z.string(),
1124
- /** Argument index carrying the subject. */
1125
- subjectArg: import_zod.z.number(),
1126
- /**
1127
- * Which argument the message body is. Left out when the method has no
1128
- * single body argument, as a batch method taking a list of entries
1129
- * does, and then no body is reported.
1130
- */
1131
- bodyArg: import_zod.z.number().optional()
1132
- }).strict();
1133
-
1134
1041
  // src/paths/gatingConditions.ts
1135
1042
  var keyOf2 = (condition) => `${condition.polarity}:${condition.sourceText}`;
1136
1043
  function sharedGatingConditions(paths, readPredicate) {