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