@suss/adapter-python 0.22.4 → 0.23.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
@@ -870,7 +870,7 @@ function passesParametersThrough(def, inner) {
870
870
 
871
871
  // src/discovery.ts
872
872
  var import_behavioral_ir2 = require("@suss/behavioral-ir");
873
- var import_extractor4 = require("@suss/extractor");
873
+ var import_extractor5 = require("@suss/extractor");
874
874
  var import_values7 = require("@suss/values");
875
875
 
876
876
  // src/envReads.ts
@@ -1079,11 +1079,11 @@ function subjectConstructions(db, valueKeys) {
1079
1079
  continue;
1080
1080
  }
1081
1081
  const deeper = calls.has(direct) ? writtenThroughCall.get(direct) : void 0;
1082
- const constructionKey = deeper ?? direct;
1082
+ const constructionKey2 = deeper ?? direct;
1083
1083
  const originOf2 = deeper === void 0 ? valueKey2 : direct;
1084
- const from = origins.get(`${originOf2}|${constructionKey}`);
1084
+ const from = origins.get(`${originOf2}|${constructionKey2}`);
1085
1085
  if (from !== void 0) {
1086
- found.set(valueKey2, { constructionKey, origins: from });
1086
+ found.set(valueKey2, { constructionKey: constructionKey2, origins: from });
1087
1087
  }
1088
1088
  }
1089
1089
  return found;
@@ -1490,7 +1490,7 @@ function emitValueFacts(db, filePath, root) {
1490
1490
  emitExpressionFacts(emitter, root);
1491
1491
  }
1492
1492
 
1493
- // src/paths/effects.ts
1493
+ // src/paths/bodyBranches.ts
1494
1494
  var import_extractor3 = require("@suss/extractor");
1495
1495
 
1496
1496
  // src/paths/lowering.ts
@@ -1766,7 +1766,89 @@ function predicateOf(node) {
1766
1766
  return opaqueOf(node);
1767
1767
  }
1768
1768
 
1769
+ // src/paths/bodyBranches.ts
1770
+ function returnStatements(node, found = []) {
1771
+ for (const child of node?.namedChildren ?? []) {
1772
+ if (child === null) {
1773
+ continue;
1774
+ }
1775
+ if (child.type === "function_definition" || child.type === "lambda") {
1776
+ continue;
1777
+ }
1778
+ if (child.type === "return_statement") {
1779
+ found.push(child);
1780
+ }
1781
+ returnStatements(child, found);
1782
+ }
1783
+ return found;
1784
+ }
1785
+ function bodyTerminals(body, raised, continuations = []) {
1786
+ const handsOn = new NodeSet(continuations);
1787
+ const found = [
1788
+ ...returnStatements(body).filter((statement) => !handsOn.has(statement)).map((statement) => ({ type: "return", statement })),
1789
+ ...raised.map(
1790
+ (response) => ({
1791
+ type: "raise",
1792
+ statement: response.statement,
1793
+ terminal: response.terminal
1794
+ })
1795
+ ),
1796
+ ...continuations.map(
1797
+ (statement) => ({ type: "continuation", statement })
1798
+ )
1799
+ ];
1800
+ return found.sort((a, b) => a.statement.startIndex - b.statement.startIndex);
1801
+ }
1802
+ function effectsReaching(effects, conditions) {
1803
+ return effects.filter(
1804
+ (effect) => (0, import_extractor3.guardsHoldOn)(effect.preconditions, conditions)
1805
+ );
1806
+ }
1807
+ function enumerateBodyBranches(options) {
1808
+ const statements = options.terminals.map((found) => found.statement);
1809
+ const leavesByCall = new NodeSet([
1810
+ ...options.raised.filter((response) => response.thrownByCall).map((response) => response.statement),
1811
+ ...options.terminals.filter((found) => found.type === "continuation").map((found) => found.statement)
1812
+ ]);
1813
+ const lowered = lowerPythonBody(options.body, statements, leavesByCall);
1814
+ const enumerated = (0, import_extractor3.enumerateOrDegrade)(
1815
+ {
1816
+ statements: lowered.statements,
1817
+ terminalsByStmt: lowered.terminalsByStmt
1818
+ },
1819
+ statements
1820
+ );
1821
+ const branches = [];
1822
+ const push = (branch, paths) => {
1823
+ for (const path6 of paths) {
1824
+ const conditions = path6.map((condition) => ({
1825
+ sourceText: condition.sourceText,
1826
+ structured: condition.expression === null ? null : predicateOf(condition.expression),
1827
+ polarity: condition.polarity,
1828
+ source: condition.source
1829
+ }));
1830
+ branches.push({
1831
+ ...branch,
1832
+ conditions,
1833
+ effects: effectsReaching(options.effects, conditions),
1834
+ isDefault: path6.length === 0
1835
+ });
1836
+ }
1837
+ };
1838
+ for (const found of options.terminals) {
1839
+ const paths = enumerated.byTerminal.get(found.statement);
1840
+ if (paths !== void 0) {
1841
+ push(options.branchOf(found), paths);
1842
+ }
1843
+ }
1844
+ if (options.fallthrough !== void 0) {
1845
+ push(options.fallthrough, enumerated.fallthrough);
1846
+ }
1847
+ return branches;
1848
+ }
1849
+
1769
1850
  // src/paths/effects.ts
1851
+ var import_extractor4 = require("@suss/extractor");
1770
1852
  var NESTED_DEFINITION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
1771
1853
  var LITERAL_ARGS = {
1772
1854
  string: (node) => ({ kind: "string", value: node.text.slice(1, -1) }),
@@ -1845,7 +1927,7 @@ function invocationEffects(definitionNode) {
1845
1927
  statements.push(statement);
1846
1928
  }
1847
1929
  const lowered = lowerPythonBody(body, statements);
1848
- const enumerated = (0, import_extractor3.enumerateOrDegrade)(
1930
+ const enumerated = (0, import_extractor4.enumerateOrDegrade)(
1849
1931
  {
1850
1932
  statements: lowered.statements,
1851
1933
  terminalsByStmt: lowered.terminalsByStmt
@@ -1854,7 +1936,7 @@ function invocationEffects(definitionNode) {
1854
1936
  );
1855
1937
  return calls.map((call) => {
1856
1938
  const statement = statementOf2.get(call.id);
1857
- const conditions = (0, import_extractor3.sharedGatingConditions)(
1939
+ const conditions = (0, import_extractor4.sharedGatingConditions)(
1858
1940
  statement === void 0 ? void 0 : enumerated.byTerminal.get(statement),
1859
1941
  predicateOf
1860
1942
  );
@@ -2734,6 +2816,18 @@ function raisedResponseOf(statement, options) {
2734
2816
  terminal: terminalOf(statement, callee, call, declared, options.facts)
2735
2817
  };
2736
2818
  }
2819
+ function returnedResponseStatus(statement, options) {
2820
+ const returned = statement.namedChildren[0];
2821
+ if (options.calls.length === 0 || returned === void 0 || returned === null || returned.type !== "call") {
2822
+ return null;
2823
+ }
2824
+ const callee = field(returned, "function");
2825
+ if (callee === null) {
2826
+ return null;
2827
+ }
2828
+ const declared = declaredCallFor(callee, options);
2829
+ return declared === void 0 ? null : statusOf(declared, returned, options.facts);
2830
+ }
2737
2831
  function calleeOf(expression, call) {
2738
2832
  return call === null ? expression : field(call, "function") ?? expression;
2739
2833
  }
@@ -3310,7 +3404,7 @@ function storageEffects(calls, options) {
3310
3404
  // src/discovery.ts
3311
3405
  function decoratedStatements(root, module2) {
3312
3406
  const found = [];
3313
- (0, import_extractor4.walkDescendants)(
3407
+ (0, import_extractor5.walkDescendants)(
3314
3408
  root,
3315
3409
  { scope: module2.moduleScope, enclosingFunction: null },
3316
3410
  {
@@ -3489,10 +3583,10 @@ function readPathArgument(classification, options) {
3489
3583
  const value = evaluatedValue(first.node, options.facts);
3490
3584
  const path6 = (0, import_values7.literalOf)(value) ?? (0, import_values7.pathOf)(value);
3491
3585
  if (path6 !== void 0) {
3492
- return (0, import_extractor4.writtenReading)(path6, classification.range);
3586
+ return (0, import_extractor5.writtenReading)(path6, classification.range);
3493
3587
  }
3494
3588
  }
3495
- return (0, import_extractor4.unreadableReading)(
3589
+ return (0, import_extractor5.unreadableReading)(
3496
3590
  "The path in this route's decorator is not a string literal, so the binding names no path and nothing pairs with it",
3497
3591
  classification.range
3498
3592
  );
@@ -3517,16 +3611,16 @@ var PATH_TEMPLATE_READERS = {
3517
3611
  function readPathTemplate(pattern, path6, range) {
3518
3612
  const syntax = pattern.pathParamSyntax;
3519
3613
  if (syntax === void 0) {
3520
- return (0, import_extractor4.writtenReading)({ path: path6, paramNames: NO_PATH_PARAMS }, range);
3614
+ return (0, import_extractor5.writtenReading)({ path: path6, paramNames: NO_PATH_PARAMS }, range);
3521
3615
  }
3522
3616
  const reader = PATH_TEMPLATE_READERS[syntax];
3523
3617
  if (reader === void 0) {
3524
- return (0, import_extractor4.unreadableReading)(
3618
+ return (0, import_extractor5.unreadableReading)(
3525
3619
  `The route's pack declares path-parameter syntax "${syntax}", which this adapter has no reader for, so the binding names no path and nothing pairs with it`,
3526
3620
  range
3527
3621
  );
3528
3622
  }
3529
- return (0, import_extractor4.writtenReading)(reader(path6), range);
3623
+ return (0, import_extractor5.writtenReading)(reader(path6), range);
3530
3624
  }
3531
3625
  function classRouteUnits(pattern, pack, classification, classNode, module2, options) {
3532
3626
  const pathArgument = readPathArgument(classification, options);
@@ -3568,7 +3662,7 @@ function classRouteUnits(pattern, pack, classification, classNode, module2, opti
3568
3662
  routePath: entry.routePath,
3569
3663
  ...entry.mount !== void 0 ? { mount: entry.mount } : {},
3570
3664
  requestBodyFromAnnotatedClass: pattern.annotatedClassIsRequestBody === true,
3571
- readsReturnedStatus: pattern.statusFromReturnedTuple === true,
3665
+ pattern,
3572
3666
  responseStatusCalls: pattern.responseStatusCalls ?? [],
3573
3667
  facts: options.facts,
3574
3668
  injectedCallees: new Set(pattern.injectedParameterCallees ?? []),
@@ -3577,12 +3671,23 @@ function classRouteUnits(pattern, pack, classification, classNode, module2, opti
3577
3671
  module: module2,
3578
3672
  filePath: options.filePath,
3579
3673
  skipReceiverParam: true,
3580
- responseShape: import_extractor4.absentReading,
3674
+ responseShape: import_extractor5.absentReading,
3581
3675
  statusCode: defaultedStatus(
3582
- readReturnedStatus(pattern, maybeMethod),
3676
+ readReturnedStatus(pattern, maybeMethod, {
3677
+ module: module2,
3678
+ facts: options.facts
3679
+ }),
3583
3680
  pattern
3584
3681
  ),
3585
- storage: options.storage
3682
+ storage: options.storage,
3683
+ wrappers: wrappersAround(
3684
+ pattern,
3685
+ pack,
3686
+ classification,
3687
+ maybeMethod,
3688
+ module2,
3689
+ options
3690
+ )
3586
3691
  },
3587
3692
  options
3588
3693
  )
@@ -3592,11 +3697,11 @@ function classRouteUnits(pattern, pack, classification, classNode, module2, opti
3592
3697
  return units;
3593
3698
  }
3594
3699
  function readRoutePath(pattern, pathArgument, classification, module2, options) {
3595
- const composed = (0, import_extractor4.andThenReading)(
3700
+ const composed = (0, import_extractor5.andThenReading)(
3596
3701
  pathArgument,
3597
3702
  (literal, range) => composeRoutePath(pattern, classification, module2, options, literal, range)
3598
3703
  );
3599
- return (0, import_extractor4.andThenReading)(
3704
+ return (0, import_extractor5.andThenReading)(
3600
3705
  composed,
3601
3706
  (path6, range) => readPathTemplate(pattern, path6, range)
3602
3707
  );
@@ -3622,10 +3727,10 @@ function readRoutePaths(pattern, pathArgument, classification, module2, options)
3622
3727
  ];
3623
3728
  }
3624
3729
  return resolution.values.map((prefix) => ({
3625
- routePath: (0, import_extractor4.andThenReading)(
3626
- (0, import_extractor4.andThenReading)(
3730
+ routePath: (0, import_extractor5.andThenReading)(
3731
+ (0, import_extractor5.andThenReading)(
3627
3732
  pathArgument,
3628
- (literal, range) => (0, import_extractor4.writtenReading)(servedSpelling(pattern, prefix + literal), range)
3733
+ (literal, range) => (0, import_extractor5.writtenReading)(servedSpelling(pattern, prefix + literal), range)
3629
3734
  ),
3630
3735
  (path6, range) => readPathTemplate(pattern, path6, range)
3631
3736
  ),
@@ -3661,9 +3766,9 @@ function composeRoutePath(pattern, classification, module2, options, literal, ra
3661
3766
  range
3662
3767
  );
3663
3768
  if (prefix.kind === "absent") {
3664
- return (0, import_extractor4.writtenReading)(servedSpelling(pattern, literal), range);
3769
+ return (0, import_extractor5.writtenReading)(servedSpelling(pattern, literal), range);
3665
3770
  }
3666
- return (0, import_extractor4.mapReading)(
3771
+ return (0, import_extractor5.mapReading)(
3667
3772
  prefix,
3668
3773
  (value) => servedSpelling(pattern, value + literal)
3669
3774
  );
@@ -3683,50 +3788,35 @@ function readRouterPrefix(pattern, classification, module2, options, range) {
3683
3788
  options
3684
3789
  );
3685
3790
  if (resolution === null) {
3686
- return import_extractor4.absentReading;
3791
+ return import_extractor5.absentReading;
3687
3792
  }
3688
3793
  if (resolution.kind === "abstain") {
3689
- return (0, import_extractor4.unreadableReading)(
3794
+ return (0, import_extractor5.unreadableReading)(
3690
3795
  `The router this route is declared on ${resolution.reason}, so the binding names no path and nothing pairs with it`,
3691
3796
  range
3692
3797
  );
3693
3798
  }
3694
3799
  if (resolution.kind === "composed") {
3695
- return (0, import_extractor4.writtenReading)(resolution.value, range);
3800
+ return (0, import_extractor5.writtenReading)(resolution.value, range);
3696
3801
  }
3697
- return import_extractor4.absentReading;
3802
+ return import_extractor5.absentReading;
3698
3803
  }
3699
3804
  function readStatusCode(pattern, classification) {
3700
3805
  if (pattern.statusCodeKeyword === void 0) {
3701
- return import_extractor4.absentReading;
3806
+ return import_extractor5.absentReading;
3702
3807
  }
3703
3808
  const arg = classification.keywordArgs[pattern.statusCodeKeyword];
3704
3809
  if (arg === void 0) {
3705
- return import_extractor4.absentReading;
3810
+ return import_extractor5.absentReading;
3706
3811
  }
3707
3812
  if (arg.kind === "number") {
3708
- return (0, import_extractor4.writtenReading)(arg.value, classification.range);
3813
+ return (0, import_extractor5.writtenReading)(arg.value, classification.range);
3709
3814
  }
3710
- return (0, import_extractor4.unreadableReading)(
3815
+ return (0, import_extractor5.unreadableReading)(
3711
3816
  "The status this route's decorator states is not a literal number, so the response claims no status",
3712
3817
  classification.range
3713
3818
  );
3714
3819
  }
3715
- function returnStatements(node, found = []) {
3716
- for (const child of node?.namedChildren ?? []) {
3717
- if (child === null) {
3718
- continue;
3719
- }
3720
- if (child.type === "function_definition" || child.type === "lambda") {
3721
- continue;
3722
- }
3723
- if (child.type === "return_statement") {
3724
- found.push(child);
3725
- }
3726
- returnStatements(child, found);
3727
- }
3728
- return found;
3729
- }
3730
3820
  function statusFromReturnedValue(node) {
3731
3821
  if (node === void 0) {
3732
3822
  return null;
@@ -3750,27 +3840,19 @@ function statusOfReturn(statement) {
3750
3840
  const status = statusFromReturnedValue(returned.namedChildren[1]);
3751
3841
  return status === null ? { kind: "unreadable" } : { kind: "status", value: status };
3752
3842
  }
3753
- function effectsReaching(effects, conditions) {
3754
- return effects.filter(
3755
- (effect) => (0, import_extractor4.guardsHoldOn)(effect.preconditions, conditions)
3756
- );
3757
- }
3758
3843
  function returnStatusReading(statement, ctx) {
3759
- if (!ctx.readsReturnedStatus) {
3760
- return ctx.declaredStatus.reading;
3761
- }
3762
- const read = statusOfReturn(statement);
3844
+ const read = statusOfReturnIn(statement, ctx.pattern, ctx);
3763
3845
  const range = rangeOf(statement);
3764
3846
  if (read.kind === "status") {
3765
- return (0, import_extractor4.writtenReading)(read.value, range);
3847
+ return (0, import_extractor5.writtenReading)(read.value, range);
3766
3848
  }
3767
- if (read.kind === "none") {
3768
- return import_extractor4.absentReading;
3849
+ if (read.kind === "unreadable") {
3850
+ return (0, import_extractor5.unreadableReading)(
3851
+ "This return gives a status this reading cannot resolve to a number, so this outcome claims none",
3852
+ range
3853
+ );
3769
3854
  }
3770
- return (0, import_extractor4.unreadableReading)(
3771
- "This return gives a status this reading cannot resolve to a number, so this outcome claims none",
3772
- range
3773
- );
3855
+ return ctx.pattern.statusFromReturnedTuple === true ? import_extractor5.absentReading : ctx.declaredStatus.reading;
3774
3856
  }
3775
3857
  function returnBranch(statement, ctx) {
3776
3858
  const range = rangeOf(statement);
@@ -3795,7 +3877,7 @@ function returnBranch(statement, ctx) {
3795
3877
  ...ctx.declaredStatus.libraryDefault !== void 0 ? { libraryDefault: ctx.declaredStatus.libraryDefault } : {}
3796
3878
  },
3797
3879
  bodyShapeReading: {
3798
- reading: writtenBody === null ? ctx.responseShape.reading : (0, import_extractor4.writtenReading)(writtenBody, range)
3880
+ reading: writtenBody === null ? ctx.responseShape.reading : (0, import_extractor5.writtenReading)(writtenBody, range)
3799
3881
  },
3800
3882
  location: range
3801
3883
  };
@@ -3806,82 +3888,57 @@ function terminalBranchOf(found, ctx) {
3806
3888
  raise: (variant) => ({
3807
3889
  terminal: variant.terminal,
3808
3890
  location: rangeOf(variant.statement)
3809
- })
3891
+ }),
3892
+ // A route has no continuation, so none is ever collected for it.
3893
+ continuation: (variant) => returnBranch(variant.statement, ctx)
3810
3894
  };
3811
3895
  return (0, import_behavioral_ir2.dispatchByType)(table, found);
3812
3896
  }
3813
- function routeTerminals(body, raised) {
3814
- const found = [
3815
- ...returnStatements(body).map(
3816
- (statement) => ({ type: "return", statement })
3817
- ),
3818
- ...raised.map(
3819
- (response) => ({
3820
- type: "raise",
3821
- statement: response.statement,
3822
- terminal: response.terminal
3823
- })
3824
- )
3825
- ];
3826
- return found.sort((a, b) => a.statement.startIndex - b.statement.startIndex);
3827
- }
3828
3897
  function branchesPerTerminal(options) {
3829
3898
  const body = field(options.definitionNode, "body");
3830
3899
  const raised = raisedResponses(body, {
3831
3900
  calls: options.responseStatusCalls,
3901
+ module: options.ctx.module,
3902
+ facts: options.ctx.facts
3903
+ });
3904
+ const terminals = bodyTerminals(body, raised);
3905
+ const soleReturnWritesBody = terminals.length === 1 && terminals[0]?.type === "return" && returnedBodyShape(terminals[0].statement) !== null;
3906
+ if (terminals.length < 2 && !soleReturnWritesBody) {
3907
+ return null;
3908
+ }
3909
+ const branches = enumerateBodyBranches({
3910
+ body,
3911
+ terminals,
3912
+ raised,
3913
+ effects: options.effects,
3914
+ branchOf: (found) => terminalBranchOf(found, options.ctx)
3915
+ });
3916
+ return branches.length > 1 || soleReturnWritesBody ? branches : null;
3917
+ }
3918
+ function statusOfReturnIn(statement, pattern, options) {
3919
+ const constructed = returnedResponseStatus(statement, {
3920
+ calls: pattern.responseConstructors ?? [],
3832
3921
  module: options.module,
3833
3922
  facts: options.facts
3834
3923
  });
3835
- const terminals = routeTerminals(body, raised);
3836
- if (terminals.length < 2) {
3837
- return null;
3924
+ if (constructed !== null) {
3925
+ return constructed.type === "literal" ? { kind: "status", value: constructed.value } : { kind: "unreadable" };
3838
3926
  }
3839
- const statements = terminals.map((found) => found.statement);
3840
- const thrownByCall = new NodeSet(
3841
- raised.filter((response) => response.thrownByCall).map((response) => response.statement)
3842
- );
3843
- const lowered = lowerPythonBody(body, statements, thrownByCall);
3844
- const enumerated = (0, import_extractor4.enumerateOrDegrade)(
3845
- {
3846
- statements: lowered.statements,
3847
- terminalsByStmt: lowered.terminalsByStmt
3848
- },
3849
- statements
3850
- );
3851
- const branches = [];
3852
- for (const found of terminals) {
3853
- const paths = enumerated.byTerminal.get(found.statement);
3854
- if (paths === void 0) {
3855
- continue;
3856
- }
3857
- const branch = terminalBranchOf(found, options.ctx);
3858
- for (const path6 of paths) {
3859
- const conditions = path6.map((condition) => ({
3860
- sourceText: condition.sourceText,
3861
- structured: condition.expression === null ? null : predicateOf(condition.expression),
3862
- polarity: condition.polarity,
3863
- source: condition.source
3864
- }));
3865
- branches.push({
3866
- ...branch,
3867
- conditions,
3868
- effects: effectsReaching(options.effects, conditions),
3869
- isDefault: path6.length === 0
3870
- });
3871
- }
3927
+ if (pattern.statusFromReturnedTuple !== true) {
3928
+ return { kind: "none" };
3872
3929
  }
3873
- return branches.length > 1 ? branches : null;
3930
+ return statusOfReturn(statement);
3874
3931
  }
3875
- function readReturnedStatus(pattern, definitionNode) {
3876
- if (pattern.statusFromReturnedTuple !== true) {
3877
- return import_extractor4.absentReading;
3932
+ function readReturnedStatus(pattern, definitionNode, options) {
3933
+ if (pattern.statusFromReturnedTuple !== true && (pattern.responseConstructors ?? []).length === 0) {
3934
+ return import_extractor5.absentReading;
3878
3935
  }
3879
3936
  const range = rangeOf(definitionNode);
3880
3937
  const statuses = /* @__PURE__ */ new Set();
3881
3938
  let plainReturns = 0;
3882
3939
  let unreadable = 0;
3883
3940
  for (const statement of returnStatements(field(definitionNode, "body"))) {
3884
- const read = statusOfReturn(statement);
3941
+ const read = statusOfReturnIn(statement, pattern, options);
3885
3942
  if (read.kind === "status") {
3886
3943
  statuses.add(read.value);
3887
3944
  } else if (read.kind === "none") {
@@ -3891,23 +3948,23 @@ function readReturnedStatus(pattern, definitionNode) {
3891
3948
  }
3892
3949
  }
3893
3950
  if (unreadable > 0) {
3894
- return (0, import_extractor4.unreadableReading)(
3951
+ return (0, import_extractor5.unreadableReading)(
3895
3952
  "This route returns a status this reading cannot resolve to a number, so no status is claimed for it",
3896
3953
  range
3897
3954
  );
3898
3955
  }
3899
3956
  if (statuses.size === 0) {
3900
- return import_extractor4.absentReading;
3957
+ return import_extractor5.absentReading;
3901
3958
  }
3902
3959
  const candidates = [...statuses];
3903
3960
  if (statuses.size > 1 || plainReturns > 0) {
3904
- return (0, import_extractor4.ambiguousReading)(
3961
+ return (0, import_extractor5.ambiguousReading)(
3905
3962
  candidates,
3906
3963
  "This route returns more than one status depending on which branch runs, and nothing here reads which, so no status is claimed for it",
3907
3964
  range
3908
3965
  );
3909
3966
  }
3910
- return (0, import_extractor4.writtenReading)(candidates[0], range);
3967
+ return (0, import_extractor5.writtenReading)(candidates[0], range);
3911
3968
  }
3912
3969
  function declaredOrReturnedStatus(declared, returned) {
3913
3970
  return declared.kind === "absent" ? returned : declared;
@@ -3920,19 +3977,19 @@ function defaultedStatus(reading, pattern) {
3920
3977
  }
3921
3978
  function readResponseModel(pattern, classification, module2, ctx) {
3922
3979
  if (pattern.responseModelKeyword === void 0) {
3923
- return import_extractor4.absentReading;
3980
+ return import_extractor5.absentReading;
3924
3981
  }
3925
3982
  const arg = classification.keywordArgs[pattern.responseModelKeyword];
3926
3983
  if (arg === void 0) {
3927
- return import_extractor4.absentReading;
3984
+ return import_extractor5.absentReading;
3928
3985
  }
3929
3986
  if (arg.kind === "identifier") {
3930
- return (0, import_extractor4.writtenReading)(
3987
+ return (0, import_extractor5.writtenReading)(
3931
3988
  shapeFromName(arg.name, module2.moduleScope, ctx),
3932
3989
  classification.range
3933
3990
  );
3934
3991
  }
3935
- return (0, import_extractor4.unreadableReading)(
3992
+ return (0, import_extractor5.unreadableReading)(
3936
3993
  "The response model this route's decorator states is not a name, so the response claims no body shape",
3937
3994
  classification.range
3938
3995
  );
@@ -3940,9 +3997,9 @@ function readResponseModel(pattern, classification, module2, ctx) {
3940
3997
  function readReturnAnnotation(definitionNode, scope, ctx) {
3941
3998
  const returnTypeNode = field(definitionNode, "return_type");
3942
3999
  if (returnTypeNode === null) {
3943
- return import_extractor4.absentReading;
4000
+ return import_extractor5.absentReading;
3944
4001
  }
3945
- return (0, import_extractor4.writtenReading)(
4002
+ return (0, import_extractor5.writtenReading)(
3946
4003
  annotationToShape(returnTypeNode, scope, ctx),
3947
4004
  rangeOf(returnTypeNode)
3948
4005
  );
@@ -3969,7 +4026,7 @@ function functionRouteUnits(pattern, pack, verb, classification, functionNode, m
3969
4026
  routePath: entry.routePath,
3970
4027
  ...entry.mount !== void 0 ? { mount: entry.mount } : {},
3971
4028
  requestBodyFromAnnotatedClass: pattern.annotatedClassIsRequestBody === true,
3972
- readsReturnedStatus: pattern.statusFromReturnedTuple === true,
4029
+ pattern,
3973
4030
  responseStatusCalls: pattern.responseStatusCalls ?? [],
3974
4031
  facts: options.facts,
3975
4032
  injectedCallees: new Set(pattern.injectedParameterCallees ?? []),
@@ -3982,22 +4039,46 @@ function functionRouteUnits(pattern, pack, verb, classification, functionNode, m
3982
4039
  statusCode: defaultedStatus(
3983
4040
  declaredOrReturnedStatus(
3984
4041
  readStatusCode(pattern, classification),
3985
- readReturnedStatus(pattern, functionNode)
4042
+ readReturnedStatus(pattern, functionNode, {
4043
+ module: module2,
4044
+ facts: options.facts
4045
+ })
3986
4046
  ),
3987
4047
  pattern
3988
4048
  ),
3989
4049
  definitionsCtx: ctx,
3990
- storage: options.storage
4050
+ storage: options.storage,
4051
+ wrappers: wrappersAround(
4052
+ pattern,
4053
+ pack,
4054
+ classification,
4055
+ functionNode,
4056
+ module2,
4057
+ options
4058
+ )
3991
4059
  },
3992
4060
  options
3993
4061
  )
3994
4062
  );
3995
4063
  }
4064
+ function wrappersAround(pattern, pack, classification, definitionNode, module2, options) {
4065
+ if (options.wrappers === void 0) {
4066
+ return [];
4067
+ }
4068
+ return options.wrappers.wrappersFor({
4069
+ pack,
4070
+ pattern,
4071
+ file: options.absoluteFile ?? options.filePath,
4072
+ module: module2,
4073
+ classification,
4074
+ definitionNode
4075
+ });
4076
+ }
3996
4077
  function readResponseShape(declared, readAnnotation) {
3997
4078
  if (declared.kind === "written") {
3998
4079
  return { reading: declared, passedOver: [] };
3999
4080
  }
4000
- return (0, import_extractor4.firstWrittenReading)([declared, readAnnotation()]);
4081
+ return (0, import_extractor5.firstWrittenReading)([declared, readAnnotation()]);
4001
4082
  }
4002
4083
  function routeUnitOrAbstention(unitOptions, options) {
4003
4084
  try {
@@ -4037,7 +4118,7 @@ function unbuiltRouteUnit(unitOptions, error) {
4037
4118
  dependencyCalls: [],
4038
4119
  declaredContract: null,
4039
4120
  readings: [
4040
- (0, import_extractor4.unreadableReading)(
4121
+ (0, import_extractor5.unreadableReading)(
4041
4122
  `This route could not be read into a unit (${message}), so the binding names no path and nothing pairs with it`,
4042
4123
  range
4043
4124
  )
@@ -4052,7 +4133,7 @@ function buildRouteUnit(options) {
4052
4133
  method,
4053
4134
  routePath,
4054
4135
  requestBodyFromAnnotatedClass,
4055
- readsReturnedStatus,
4136
+ pattern,
4056
4137
  injectedCallees,
4057
4138
  definitionNode,
4058
4139
  enclosingScope,
@@ -4063,7 +4144,7 @@ function buildRouteUnit(options) {
4063
4144
  definitionsCtx,
4064
4145
  storage: storageLookup
4065
4146
  } = options;
4066
- const template = (0, import_extractor4.valueToReadFurtherFrom)(routePath);
4147
+ const template = (0, import_extractor5.valueToReadFurtherFrom)(routePath);
4067
4148
  const ctx = definitionsCtx ?? createAnnotationContext(module2.scopeFor);
4068
4149
  const parameters = readParameters(
4069
4150
  definitionNode,
@@ -4083,10 +4164,14 @@ function buildRouteUnit(options) {
4083
4164
  const perTerminal = branchesPerTerminal({
4084
4165
  definitionNode,
4085
4166
  effects,
4086
- module: module2,
4087
- facts: options.facts,
4088
4167
  responseStatusCalls: options.responseStatusCalls,
4089
- ctx: { readsReturnedStatus, responseShape, declaredStatus: statusCode }
4168
+ ctx: {
4169
+ pattern,
4170
+ responseShape,
4171
+ declaredStatus: statusCode,
4172
+ module: module2,
4173
+ facts: options.facts
4174
+ }
4090
4175
  });
4091
4176
  const branches = (perTerminal ?? []).map(
4092
4177
  (branch) => extra.length === 0 ? branch : { ...branch, extraEffects: extra }
@@ -4145,7 +4230,8 @@ function buildRouteUnit(options) {
4145
4230
  ...responseShape.passedOver
4146
4231
  ],
4147
4232
  ...collectedDefinitions(ctx) !== null ? { definitions: collectedDefinitions(ctx) } : {},
4148
- ...options.mount !== void 0 ? { mount: options.mount } : {}
4233
+ ...options.mount !== void 0 ? { mount: options.mount } : {},
4234
+ ...options.wrappers !== void 0 && options.wrappers.length > 0 ? { wrappers: options.wrappers } : {}
4149
4235
  };
4150
4236
  }
4151
4237
  function unreadRoleReadings(parameters, range) {
@@ -4153,7 +4239,7 @@ function unreadRoleReadings(parameters, range) {
4153
4239
  return [];
4154
4240
  }
4155
4241
  return [
4156
- (0, import_extractor4.unreadableReading)(
4242
+ (0, import_extractor5.unreadableReading)(
4157
4243
  "Nothing read this route's path, so its parameters name no role and a path parameter here does not read as one",
4158
4244
  range
4159
4245
  )
@@ -4494,12 +4580,12 @@ var import_node_fs3 = __toESM(require("fs"), 1);
4494
4580
  var import_node_path3 = __toESM(require("path"), 1);
4495
4581
  var import_behavioral_ir4 = require("@suss/behavioral-ir");
4496
4582
  var import_datalog3 = require("@suss/datalog");
4497
- var import_extractor8 = require("@suss/extractor");
4583
+ var import_extractor10 = require("@suss/extractor");
4498
4584
 
4499
4585
  // src/diagnostics.ts
4500
- var import_extractor5 = require("@suss/extractor");
4586
+ var import_extractor6 = require("@suss/extractor");
4501
4587
  function buildPythonExtractionReport(args) {
4502
- return (0, import_extractor5.buildUngatedExtractionReport)({
4588
+ return (0, import_extractor6.buildUngatedExtractionReport)({
4503
4589
  packs: args.packs.map((pack) => ({
4504
4590
  name: pack.name,
4505
4591
  version: pack.version ?? null,
@@ -4514,7 +4600,7 @@ function buildPythonExtractionReport(args) {
4514
4600
  // src/reach/closure.ts
4515
4601
  var import_behavioral_ir3 = require("@suss/behavioral-ir");
4516
4602
  var import_datalog2 = require("@suss/datalog");
4517
- var import_extractor6 = require("@suss/extractor");
4603
+ var import_extractor7 = require("@suss/extractor");
4518
4604
 
4519
4605
  // src/reach/resolveCallee.ts
4520
4606
  var stop = (reason) => ({ kind: "stop", reason });
@@ -4839,7 +4925,7 @@ function reachedFunctions(seeds, options) {
4839
4925
  if (seedKeys.has(key) || target === void 0) {
4840
4926
  continue;
4841
4927
  }
4842
- const summary = (0, import_extractor6.assembleSummary)(libraryUnit(target, options), {
4928
+ const summary = (0, import_extractor7.assembleSummary)(libraryUnit(target, options), {
4843
4929
  gapHandling: options.gapHandling
4844
4930
  });
4845
4931
  summary.confidence = { source: "inferred_static", level: "low" };
@@ -4959,7 +5045,7 @@ function scanBody(source, ctx, recognized) {
4959
5045
  followed.push(outcome.target);
4960
5046
  }
4961
5047
  };
4962
- (0, import_extractor6.walkDescendants)(
5048
+ (0, import_extractor7.walkDescendants)(
4963
5049
  body,
4964
5050
  { scope: scopeAt2(file, node), rebound: reboundNames(body) },
4965
5051
  {
@@ -4970,7 +5056,7 @@ function scanBody(source, ctx, recognized) {
4970
5056
  },
4971
5057
  into: (child, where) => {
4972
5058
  if (child.type === "function_definition") {
4973
- return import_extractor6.SKIP_CHILDREN;
5059
+ return import_extractor7.SKIP_CHILDREN;
4974
5060
  }
4975
5061
  if (child.type === "lambda") {
4976
5062
  return {
@@ -5015,7 +5101,7 @@ var REBINDING_FIELDS = {
5015
5101
  };
5016
5102
  function reboundNames(body) {
5017
5103
  const names = /* @__PURE__ */ new Set();
5018
- (0, import_extractor6.walkDescendants)(body, null, {
5104
+ (0, import_extractor7.walkDescendants)(body, null, {
5019
5105
  at: (node) => {
5020
5106
  const target = REBINDING_FIELDS[node.type];
5021
5107
  if (target === void 0 || boundByBinder(node, body)) {
@@ -5028,7 +5114,7 @@ function reboundNames(body) {
5028
5114
  }
5029
5115
  }
5030
5116
  },
5031
- into: (node) => node.type === "function_definition" ? import_extractor6.SKIP_CHILDREN : null
5117
+ into: (node) => node.type === "function_definition" ? import_extractor7.SKIP_CHILDREN : null
5032
5118
  });
5033
5119
  return names;
5034
5120
  }
@@ -5193,12 +5279,8 @@ function buildRouterIndex(files, packs, resolverOptions) {
5193
5279
  }
5194
5280
  return {
5195
5281
  moduleDef(fromFile, spec, name) {
5196
- const resolution = resolveModule(fromFile, spec, resolverOptions);
5197
- if (resolution.status !== "resolved") {
5198
- return null;
5199
- }
5200
- const bound = byFile.get(resolution.file);
5201
- if (bound === void 0) {
5282
+ const bound = boundModuleAt(fromFile, spec, byFile, resolverOptions);
5283
+ if (bound === null) {
5202
5284
  return null;
5203
5285
  }
5204
5286
  const binding = bound.module.moduleScope.bindings.get(name);
@@ -5224,12 +5306,12 @@ function buildRouterIndex(files, packs, resolverOptions) {
5224
5306
  }
5225
5307
  return composedPrefixOf(index, construction, displayPaths.get(module2));
5226
5308
  },
5227
- resolveConstruction(pattern, module2, constructorName, constructionKey) {
5309
+ resolveConstruction(pattern, module2, constructorName, constructionKey2) {
5228
5310
  const index = byPattern.get(pattern);
5229
5311
  if (index === void 0) {
5230
5312
  return NOT_ROUTER;
5231
5313
  }
5232
- const construction = index.byValueKey.get(constructionKey);
5314
+ const construction = index.byValueKey.get(constructionKey2);
5233
5315
  if (construction !== void 0) {
5234
5316
  return composedPrefixOf(index, construction, displayPaths.get(module2));
5235
5317
  }
@@ -5465,6 +5547,13 @@ function buildPatternIndex(files, importModule, composition, resolverOptions) {
5465
5547
  }
5466
5548
  return index;
5467
5549
  }
5550
+ function boundModuleAt(fromFile, spec, byFile, resolverOptions) {
5551
+ const resolution = resolveModule(fromFile, spec, resolverOptions);
5552
+ if (resolution.status !== "resolved") {
5553
+ return null;
5554
+ }
5555
+ return byFile.get(resolution.file) ?? null;
5556
+ }
5468
5557
  function constructionOf(name, scope, importModule) {
5469
5558
  const binding = resolveName(scope, name);
5470
5559
  if (binding?.kind !== "assignment" || binding.value?.type !== "call") {
@@ -5673,16 +5762,13 @@ function constructionNamed(name, scope, scan, constructions = scan.index.constru
5673
5762
  if (binding.kind !== "importFrom") {
5674
5763
  return null;
5675
5764
  }
5676
- const resolution = resolveModule(
5765
+ const target = boundModuleAt(
5677
5766
  scan.bound.file,
5678
5767
  { module: binding.module, relativeLevel: binding.relativeLevel },
5768
+ scan.byFile,
5679
5769
  scan.resolverOptions
5680
5770
  );
5681
- if (resolution.status !== "resolved") {
5682
- return null;
5683
- }
5684
- const target = scan.byFile.get(resolution.file);
5685
- if (target === void 0) {
5771
+ if (target === null) {
5686
5772
  return null;
5687
5773
  }
5688
5774
  return constructions.get(target.module)?.get(binding.importedName) ?? null;
@@ -5700,10 +5786,10 @@ function constructionThroughFacts(name, scope, scan) {
5700
5786
  return null;
5701
5787
  }
5702
5788
  const nameKey = `${scan.bound.file}#${name}`;
5703
- const constructionKey = subjectConstructions(facts, [nameKey]).get(
5789
+ const constructionKey2 = subjectConstructions(facts, [nameKey]).get(
5704
5790
  nameKey
5705
5791
  )?.constructionKey;
5706
- return constructionKey === void 0 ? null : scan.index.byValueKey.get(constructionKey) ?? null;
5792
+ return constructionKey2 === void 0 ? null : scan.index.byValueKey.get(constructionKey2) ?? null;
5707
5793
  }
5708
5794
  var MOUNT_STATE_BY_EFFECT = {
5709
5795
  prefixes: (statedPrefix, site) => ({
@@ -5940,12 +6026,13 @@ function constructionThroughModule(arg, scope, scan) {
5940
6026
  if (spec === null) {
5941
6027
  return null;
5942
6028
  }
5943
- const resolution = resolveModule(scan.bound.file, spec, scan.resolverOptions);
5944
- if (resolution.status !== "resolved") {
5945
- return null;
5946
- }
5947
- const target = scan.byFile.get(resolution.file);
5948
- if (target === void 0) {
6029
+ const target = boundModuleAt(
6030
+ scan.bound.file,
6031
+ spec,
6032
+ scan.byFile,
6033
+ scan.resolverOptions
6034
+ );
6035
+ if (target === null) {
5949
6036
  return null;
5950
6037
  }
5951
6038
  return scan.index.constructions.get(target.module)?.get(arg.attributeName) ?? null;
@@ -6289,13 +6376,479 @@ function carrierPrefix(carrier, constructorArg, objectName, scope, scan) {
6289
6376
  }
6290
6377
 
6291
6378
  // src/version.ts
6292
- var import_extractor7 = require("@suss/extractor");
6379
+ var import_extractor8 = require("@suss/extractor");
6293
6380
  var ADAPTER_VERSION = "0.1.0";
6294
- var adapterStamp = (0, import_extractor7.createAdapterStamp)({
6381
+ var adapterStamp = (0, import_extractor8.createAdapterStamp)({
6295
6382
  moduleUrl: importMetaUrl,
6296
6383
  version: ADAPTER_VERSION
6297
6384
  });
6298
6385
 
6386
+ // src/wrappers.ts
6387
+ var import_extractor9 = require("@suss/extractor");
6388
+ function buildWrapperIndex(files, options) {
6389
+ const forms = declaredForms(options.packs);
6390
+ const filesByPath = new Map(files.map((file) => [file.file, file]));
6391
+ const index = new PythonWrapperIndex(options, filesByPath);
6392
+ if (forms.length > 0) {
6393
+ for (const file of files) {
6394
+ for (const declared of forms) {
6395
+ registerFrom(file, declared, index);
6396
+ }
6397
+ }
6398
+ }
6399
+ return index;
6400
+ }
6401
+ function declaredForms(packs) {
6402
+ return packs.flatMap(
6403
+ (pack) => pack.discovery.flatMap(
6404
+ (pattern) => (pattern.wrappers ?? []).map((form) => ({ pack, pattern, form }))
6405
+ )
6406
+ );
6407
+ }
6408
+ var PythonWrapperIndex = class {
6409
+ constructor(options, filesByPath) {
6410
+ this.options = options;
6411
+ this.filesByPath = filesByPath;
6412
+ }
6413
+ options;
6414
+ filesByPath;
6415
+ /** By pack name: what the app registered, in the order the forms are declared and then the order the files were read. */
6416
+ everyRoute = /* @__PURE__ */ new Map();
6417
+ /** By construction key: what a router or a blueprint registered. */
6418
+ ownRoutes = /* @__PURE__ */ new Map();
6419
+ unitsByKey = /* @__PURE__ */ new Map();
6420
+ unitsByFile = /* @__PURE__ */ new Map();
6421
+ register(covers, target, declared) {
6422
+ const registered = this.registered(target, declared);
6423
+ const table = covers.kind === "everyRoute" ? this.everyRoute : this.ownRoutes;
6424
+ const key = covers.kind === "everyRoute" ? covers.pack : covers.key;
6425
+ const list = table.get(key) ?? [];
6426
+ list.push(registered);
6427
+ table.set(key, list);
6428
+ }
6429
+ /** The unit for a function, built once however many registrations point at it. */
6430
+ registered(target, declared) {
6431
+ const span = spanOf(target.node);
6432
+ const key = `${target.file.file}:${span.start}-${span.end}`;
6433
+ if (!this.unitsByKey.has(key)) {
6434
+ const unit = wrapperUnit(target, declared, this.options);
6435
+ this.unitsByKey.set(key, unit);
6436
+ const inFile = this.unitsByFile.get(target.file.file) ?? [];
6437
+ inFile.push(unit);
6438
+ this.unitsByFile.set(target.file.file, inFile);
6439
+ }
6440
+ return {
6441
+ reference: {
6442
+ file: target.file.displayPath,
6443
+ name: target.name,
6444
+ ...isThrowForm(declared.form) ? { onThrow: true } : {}
6445
+ },
6446
+ form: declared.form
6447
+ };
6448
+ }
6449
+ wrappersFor(query) {
6450
+ const file = this.filesByPath.get(query.file);
6451
+ const forms = (query.pattern.wrappers ?? []).map((form) => ({
6452
+ pack: query.pack,
6453
+ pattern: query.pattern,
6454
+ form
6455
+ }));
6456
+ if (file === void 0 || forms.length === 0) {
6457
+ return [];
6458
+ }
6459
+ const found = [
6460
+ ...this.everyRoute.get(query.pack.name) ?? [],
6461
+ ...this.ownRoutesOf(query)
6462
+ ];
6463
+ for (const declared of forms) {
6464
+ if (declared.form.type !== "dependency") {
6465
+ continue;
6466
+ }
6467
+ const site = {
6468
+ file,
6469
+ scope: query.module.moduleScope,
6470
+ rebound: /* @__PURE__ */ new Set()
6471
+ };
6472
+ const targets = [
6473
+ ...decoratorDependencies(query.classification, declared.form),
6474
+ ...parameterDependencies(query.definitionNode, declared.form)
6475
+ ];
6476
+ for (const name of targets) {
6477
+ const target = resolveNamedFunctionArgument(name, site, {
6478
+ filesByPath: this.filesByPath,
6479
+ roots: this.options.roots
6480
+ });
6481
+ if (target !== null) {
6482
+ found.push(this.registered(target, declared));
6483
+ }
6484
+ }
6485
+ }
6486
+ const seen = /* @__PURE__ */ new Set();
6487
+ const ordered = [];
6488
+ for (const wrapper of [...found].sort(
6489
+ (a, b) => runOrder(a.form) - runOrder(b.form)
6490
+ )) {
6491
+ const key = `${wrapper.reference.file}::${wrapper.reference.name}::${wrapper.reference.onThrow === true}`;
6492
+ if (seen.has(key)) {
6493
+ continue;
6494
+ }
6495
+ seen.add(key);
6496
+ ordered.push(wrapper.reference);
6497
+ }
6498
+ return ordered;
6499
+ }
6500
+ ownRoutesOf(query) {
6501
+ const site = this.objectSiteOf(query);
6502
+ if (site === null) {
6503
+ return [];
6504
+ }
6505
+ for (const form of query.pattern.wrappers ?? []) {
6506
+ const match = registrarOf(site.name, site.scope, {
6507
+ pack: query.pack,
6508
+ pattern: query.pattern,
6509
+ form
6510
+ });
6511
+ if (match !== null && match.registrar.covers === "ownRoutes") {
6512
+ return this.ownRoutes.get(constructionKey(site.file, match.call)) ?? [];
6513
+ }
6514
+ }
6515
+ return [];
6516
+ }
6517
+ /**
6518
+ * Where the decorator's object was built: the route's own file, the file
6519
+ * a project wrapper function is written in, or the file an imported
6520
+ * router comes from. The name is the one that file binds it under.
6521
+ */
6522
+ objectSiteOf(query) {
6523
+ const name = query.classification.objectName;
6524
+ if (name === null) {
6525
+ return null;
6526
+ }
6527
+ const wrapperModule = query.classification.objectModule;
6528
+ if (wrapperModule !== void 0) {
6529
+ const file = this.fileOfModule(wrapperModule);
6530
+ return file === null ? null : { file, scope: wrapperModule.moduleScope, name };
6531
+ }
6532
+ const binding = resolveName(query.module.moduleScope, name);
6533
+ if (binding?.kind !== "importFrom") {
6534
+ return { file: query.file, scope: query.module.moduleScope, name };
6535
+ }
6536
+ const target = boundModuleAt(
6537
+ query.file,
6538
+ { module: binding.module, relativeLevel: binding.relativeLevel },
6539
+ this.filesByPath,
6540
+ { roots: this.options.roots }
6541
+ );
6542
+ return target === null ? null : {
6543
+ file: target.file,
6544
+ scope: target.module.moduleScope,
6545
+ name: binding.importedName
6546
+ };
6547
+ }
6548
+ fileOfModule(module2) {
6549
+ for (const file of this.filesByPath.values()) {
6550
+ if (file.module === module2) {
6551
+ return file.file;
6552
+ }
6553
+ }
6554
+ return null;
6555
+ }
6556
+ unitsIn(file) {
6557
+ return this.unitsByFile.get(file) ?? [];
6558
+ }
6559
+ };
6560
+ function isThrowForm(form) {
6561
+ return form.type === "decoratedWrapper" && form.throwParam !== void 0;
6562
+ }
6563
+ function runOrder(form) {
6564
+ if (isThrowForm(form)) {
6565
+ return 2;
6566
+ }
6567
+ return form.type === "decoratedWrapper" ? 0 : 1;
6568
+ }
6569
+ function registrarOf(name, scope, declared) {
6570
+ const modules = [
6571
+ ...declared.pattern.importModule,
6572
+ ...declared.form.registrars.flatMap(
6573
+ (registrar2) => registrar2.importModule ?? []
6574
+ )
6575
+ ];
6576
+ const construction = constructionOf(name, scope, modules);
6577
+ if (construction === null) {
6578
+ return null;
6579
+ }
6580
+ const registrar = declared.form.registrars.find(
6581
+ (candidate) => candidate.constructorName === construction.constructorName
6582
+ );
6583
+ return registrar === void 0 ? null : { registrar, call: construction.call };
6584
+ }
6585
+ function constructionKey(file, call) {
6586
+ return `${file}#${call.startIndex}`;
6587
+ }
6588
+ function registerFrom(file, declared, index) {
6589
+ const table = {
6590
+ dependency: () => registerConstructorDependencies(
6591
+ file,
6592
+ declared,
6593
+ declared.form,
6594
+ index
6595
+ ),
6596
+ decoratedWrapper: () => registerDecorated(
6597
+ file,
6598
+ declared,
6599
+ declared.form,
6600
+ index
6601
+ )
6602
+ };
6603
+ table[declared.form.type]();
6604
+ }
6605
+ function registerConstructorDependencies(file, declared, form, index) {
6606
+ const scope = file.module.moduleScope;
6607
+ for (const stmt of file.root.namedChildren) {
6608
+ const assignment2 = stmt?.type === "expression_statement" ? stmt.namedChildren.find((child) => child?.type === "assignment") : void 0;
6609
+ const left = assignment2 === void 0 || assignment2 === null ? null : field(assignment2, "left");
6610
+ if (left?.type !== "identifier") {
6611
+ continue;
6612
+ }
6613
+ const match = registrarOf(left.text, scope, declared);
6614
+ if (match === null) {
6615
+ continue;
6616
+ }
6617
+ const { keywordArgs } = readCallArguments(field(match.call, "arguments"));
6618
+ const listed = keywordArgs[form.keyword];
6619
+ if (listed === void 0) {
6620
+ continue;
6621
+ }
6622
+ const site = { file, scope, rebound: /* @__PURE__ */ new Set() };
6623
+ for (const name of dependencyNamesIn(listed.node, form)) {
6624
+ const target = resolveNamedFunctionArgument(name, site, {
6625
+ filesByPath: index.filesByPath,
6626
+ roots: index.options.roots
6627
+ });
6628
+ if (target !== null) {
6629
+ index.register(
6630
+ coverageOf(match, file, declared.pack),
6631
+ target,
6632
+ declared
6633
+ );
6634
+ }
6635
+ }
6636
+ }
6637
+ }
6638
+ function coverageOf(match, file, pack) {
6639
+ return match.registrar.covers === "everyRoute" ? { kind: "everyRoute", pack: pack.name } : { kind: "ownRoutes", key: constructionKey(file.file, match.call) };
6640
+ }
6641
+ function registerDecorated(file, declared, form, index) {
6642
+ (0, import_extractor9.walkDescendants)(file.root, file.module.moduleScope, {
6643
+ at: (node, scope) => {
6644
+ if (node.type !== "decorated_definition") {
6645
+ return;
6646
+ }
6647
+ const { definition, decorators } = stripDecorators(node);
6648
+ if (definition.type !== "function_definition") {
6649
+ return;
6650
+ }
6651
+ for (const decorator of decorators) {
6652
+ const receiver = decoratorReceiver(decorator);
6653
+ if (receiver === null || receiver.attributeName !== form.attribute || receiver.object.type !== "identifier") {
6654
+ continue;
6655
+ }
6656
+ const match = registrarOf(receiver.object.text, scope, declared);
6657
+ const name = field(definition, "name")?.text;
6658
+ if (match === null || name === void 0) {
6659
+ continue;
6660
+ }
6661
+ index.register(
6662
+ coverageOf(match, file, declared.pack),
6663
+ { file, node: definition, name, exportPath: [name] },
6664
+ declared
6665
+ );
6666
+ }
6667
+ },
6668
+ into: (node, scope) => file.module.scopeFor.get(node.id) ?? scope
6669
+ });
6670
+ }
6671
+ function dependencyNamesIn(node, form) {
6672
+ const calls = node.type === "list" ? node.namedChildren : [node];
6673
+ const names = [];
6674
+ for (const call of calls) {
6675
+ const name = call === null ? null : dependencyNameOf(call, form);
6676
+ if (name !== null) {
6677
+ names.push(name);
6678
+ }
6679
+ }
6680
+ return names;
6681
+ }
6682
+ function dependencyNameOf(node, form) {
6683
+ if (node.type !== "call") {
6684
+ return null;
6685
+ }
6686
+ const callee = field(node, "function");
6687
+ const calleeName3 = callee?.type === "identifier" ? callee.text : callee?.type === "attribute" ? field(callee, "attribute")?.text ?? null : null;
6688
+ if (calleeName3 === null || !form.callees.includes(calleeName3)) {
6689
+ return null;
6690
+ }
6691
+ const first = field(node, "arguments")?.namedChildren[0];
6692
+ return first?.type === "identifier" ? first.text : null;
6693
+ }
6694
+ function decoratorDependencies(classification, form) {
6695
+ const listed = classification.keywordArgs[form.keyword];
6696
+ return listed === void 0 ? [] : dependencyNamesIn(listed.node, form);
6697
+ }
6698
+ var MAX_ANNOTATION_DEPTH2 = 6;
6699
+ function parameterDependencies(definitionNode, form) {
6700
+ const names = [];
6701
+ const findIn = (node, depth) => {
6702
+ if (node === null || depth > MAX_ANNOTATION_DEPTH2) {
6703
+ return null;
6704
+ }
6705
+ const direct = dependencyNameOf(node, form);
6706
+ if (direct !== null) {
6707
+ return direct;
6708
+ }
6709
+ for (const child of node.namedChildren) {
6710
+ const found = findIn(child, depth + 1);
6711
+ if (found !== null) {
6712
+ return found;
6713
+ }
6714
+ }
6715
+ return null;
6716
+ };
6717
+ for (const param of field(definitionNode, "parameters")?.namedChildren ?? []) {
6718
+ if (param === null) {
6719
+ continue;
6720
+ }
6721
+ const inDefault = field(param, "value");
6722
+ const found = inDefault === null ? findIn(field(param, "type"), 0) : dependencyNameOf(inDefault, form);
6723
+ if (found !== null) {
6724
+ names.push(found);
6725
+ }
6726
+ }
6727
+ return names;
6728
+ }
6729
+ function wrapperUnit(target, declared, options) {
6730
+ const { file, node, name, exportPath } = target;
6731
+ const body = field(node, "body");
6732
+ const range = rangeOf(node);
6733
+ const raised = raisedResponses(body, {
6734
+ calls: declared.pattern.responseStatusCalls ?? [],
6735
+ module: file.module,
6736
+ facts: options.facts
6737
+ });
6738
+ const continuations = continuationStatements(node, declared.form);
6739
+ const terminals = bodyTerminals(body, raised, continuations);
6740
+ const effects = invocationEffects(node);
6741
+ const extra = recognizedBodyEffects(
6742
+ node,
6743
+ file.module,
6744
+ options.storageFor(file)
6745
+ );
6746
+ const delegate = (at) => ({
6747
+ terminal: {
6748
+ kind: "delegate",
6749
+ statusCode: null,
6750
+ body: null,
6751
+ exceptionType: null,
6752
+ message: null,
6753
+ component: null,
6754
+ renderTree: null,
6755
+ delegateTarget: null,
6756
+ emitEvent: null,
6757
+ location: rangeOf(at)
6758
+ },
6759
+ location: rangeOf(at)
6760
+ });
6761
+ const respond = (statement) => returnBranch(statement, {
6762
+ pattern: declared.pattern,
6763
+ responseShape: { reading: import_extractor9.absentReading },
6764
+ declaredStatus: {
6765
+ reading: import_extractor9.absentReading,
6766
+ ...declared.pattern.defaultStatusCode !== void 0 ? { libraryDefault: declared.pattern.defaultStatusCode } : {}
6767
+ },
6768
+ module: file.module,
6769
+ facts: options.facts
6770
+ });
6771
+ const branchOf = (terminal) => {
6772
+ if (terminal.type === "raise") {
6773
+ return {
6774
+ terminal: terminal.terminal,
6775
+ location: rangeOf(terminal.statement)
6776
+ };
6777
+ }
6778
+ if (terminal.type === "continuation" || returnHandsOn(terminal.statement, declared.form)) {
6779
+ return delegate(terminal.statement);
6780
+ }
6781
+ return respond(terminal.statement);
6782
+ };
6783
+ const branches = enumerateBodyBranches({
6784
+ body,
6785
+ terminals,
6786
+ raised,
6787
+ effects,
6788
+ branchOf,
6789
+ fallthrough: delegate(node)
6790
+ }).map(
6791
+ (branch) => extra.length === 0 ? branch : { ...branch, extraEffects: extra }
6792
+ );
6793
+ return {
6794
+ identity: {
6795
+ name,
6796
+ nameKind: "binding",
6797
+ kind: "middleware",
6798
+ file: file.displayPath,
6799
+ range,
6800
+ span: spanOf(node),
6801
+ exportName: exportPath[0] ?? name,
6802
+ exportPath
6803
+ },
6804
+ boundaryBinding: null,
6805
+ parameters: [],
6806
+ branches,
6807
+ bodyContent: body === null ? "absent" : bodyContentOf(body),
6808
+ dependencyCalls: [],
6809
+ declaredContract: null
6810
+ };
6811
+ }
6812
+ function returnHandsOn(statement, form) {
6813
+ if (form.type === "dependency") {
6814
+ return true;
6815
+ }
6816
+ if (form.continuationParam !== void 0 || form.throwParam !== void 0) {
6817
+ return false;
6818
+ }
6819
+ if (form.returnedValueResponds === true) {
6820
+ const returned = statement.namedChildren[0];
6821
+ return returned === void 0 || returned === null || returned.type === "none";
6822
+ }
6823
+ return true;
6824
+ }
6825
+ function continuationStatements(node, form) {
6826
+ if (form.type !== "decoratedWrapper" || form.continuationParam === void 0) {
6827
+ return [];
6828
+ }
6829
+ const params = (field(node, "parameters")?.namedChildren ?? []).filter(
6830
+ (param2) => param2 !== null
6831
+ );
6832
+ const param = params[form.continuationParam];
6833
+ const continuation = param === void 0 ? null : field(param, "name")?.text ?? (param.type === "identifier" ? param.text : null);
6834
+ if (continuation === null) {
6835
+ return [];
6836
+ }
6837
+ const body = field(node, "body");
6838
+ if (body === null) {
6839
+ return [];
6840
+ }
6841
+ const found = [];
6842
+ for (const call of bodyCalls(body)) {
6843
+ const callee = field(call, "function");
6844
+ const statement = callee?.type === "identifier" && callee.text === continuation ? enclosingStatement(call, body) : null;
6845
+ if (statement !== null) {
6846
+ found.push(statement);
6847
+ }
6848
+ }
6849
+ return found;
6850
+ }
6851
+
6299
6852
  // src/project.ts
6300
6853
  function reportUnresolvedProjectModules(options, db) {
6301
6854
  const imported = new Set(db.facts("pyImport").map((row) => String(row[1])));
@@ -6312,11 +6865,11 @@ function reportUnresolvedProjectModules(options, db) {
6312
6865
  }
6313
6866
  }
6314
6867
  async function extractPythonProject(options) {
6315
- const timer = options.onTiming !== void 0 ? (0, import_extractor8.createTimer)() : (0, import_extractor8.noopTimer)();
6868
+ const timer = options.onTiming !== void 0 ? (0, import_extractor10.createTimer)() : (0, import_extractor10.noopTimer)();
6316
6869
  const cacheDir = adapterStamp.declineWhenRunFromSource(
6317
6870
  options.cacheDir === null ? null : options.cacheDir ?? (options.projectRoot !== void 0 ? import_node_path3.default.join(options.projectRoot, ".suss", "cache") : null)
6318
6871
  );
6319
- const cache = (0, import_extractor8.createCacheLayer)(cacheDir);
6872
+ const cache = (0, import_extractor10.createCacheLayer)(cacheDir);
6320
6873
  const packsDigest = adapterStamp.packsDigest(
6321
6874
  options.packs.map(
6322
6875
  (pack) => pack.version !== void 0 ? { name: pack.name, version: pack.version } : { name: pack.name }
@@ -6324,7 +6877,7 @@ async function extractPythonProject(options) {
6324
6877
  );
6325
6878
  const cacheInput = {
6326
6879
  files: cacheDir === null ? [] : options.files,
6327
- adapterPacksDigest: cacheDir === null ? packsDigest : (0, import_extractor8.runDigest)(packsDigest, options.packs, options.files)
6880
+ adapterPacksDigest: cacheDir === null ? packsDigest : (0, import_extractor10.runDigest)(packsDigest, options.packs, options.files)
6328
6881
  };
6329
6882
  const lookup = await timer.timeAsync(
6330
6883
  "cache.lookup",
@@ -6338,7 +6891,7 @@ async function extractPythonProject(options) {
6338
6891
  const db = new import_datalog3.Database();
6339
6892
  const summaries = [];
6340
6893
  const gapHandling = options.gapHandling ?? "permissive";
6341
- const tallies = (0, import_extractor5.createPackTallies)(options.packs);
6894
+ const tallies = (0, import_extractor6.createPackTallies)(options.packs);
6342
6895
  const displayPathOf = (file) => options.workspaceRoot !== void 0 ? import_node_path3.default.relative(options.workspaceRoot, file) : file;
6343
6896
  const bound = [];
6344
6897
  for (const file of options.files) {
@@ -6393,33 +6946,52 @@ async function extractPythonProject(options) {
6393
6946
  couldMatch,
6394
6947
  rawSql: rawSqlPatterns
6395
6948
  } : void 0;
6396
- const seeds = [];
6397
- const summariesBySeed = /* @__PURE__ */ new Map();
6949
+ const wrapperIndex = timer.time(
6950
+ "discover",
6951
+ () => buildWrapperIndex(bound, {
6952
+ packs: options.packs,
6953
+ roots: options.roots,
6954
+ facts: needsValues ? db : void 0,
6955
+ storageFor
6956
+ })
6957
+ );
6958
+ const discovered = /* @__PURE__ */ new Map();
6398
6959
  for (const boundFile of bound) {
6399
6960
  const { file, root, module: moduleBinding } = boundFile;
6400
- const displayPath = displayPathOf(file);
6401
6961
  const storage = storageFor(boundFile);
6402
6962
  const rawUnits = timer.time(
6403
6963
  "discover",
6404
6964
  () => discoverUnits(root, moduleBinding, {
6405
6965
  packs: options.packs,
6406
- filePath: displayPath,
6966
+ filePath: displayPathOf(file),
6407
6967
  absoluteFile: file,
6408
6968
  routerIndex,
6409
6969
  gapHandling,
6970
+ wrappers: wrapperIndex,
6410
6971
  ...needsValues ? { facts: db } : {},
6411
6972
  ...storage === void 0 ? {} : { storage }
6412
6973
  })
6413
6974
  );
6975
+ discovered.set(file, rawUnits);
6976
+ }
6977
+ const seeds = [];
6978
+ const summariesBySeed = /* @__PURE__ */ new Map();
6979
+ for (const boundFile of bound) {
6980
+ const { file, root, module: moduleBinding } = boundFile;
6981
+ const displayPath = displayPathOf(file);
6982
+ const rawUnits = [
6983
+ ...discovered.get(file) ?? [],
6984
+ ...wrapperIndex.unitsIn(file)
6985
+ ];
6414
6986
  for (const raw of rawUnits) {
6415
6987
  const summary = timer.time(
6416
6988
  "summarize",
6417
- () => (0, import_extractor8.assembleSummary)(raw, { gapHandling })
6989
+ () => (0, import_extractor10.assembleSummary)(raw, { gapHandling })
6418
6990
  );
6419
6991
  summary.confidence = { source: "inferred_static", level: "low" };
6420
6992
  summaries.push(summary);
6421
6993
  emitEntryFact(db, file, raw.identity.range, raw.identity.name);
6422
- (0, import_extractor5.tallyUnit)(tallies, raw.boundaryBinding?.recognition);
6994
+ (0, import_extractor6.tallyUnit)(tallies, raw.boundaryBinding?.recognition);
6423
6995
  const span = raw.identity.span;
6424
6996
  const key = span === void 0 ? null : `${file}:${span.start}-${span.end}`;
6425
6997
  const node = key === null ? void 0 : definitions.get(key);
@@ -6441,8 +7013,8 @@ async function extractPythonProject(options) {
6441
7013
  if (loadTimeReads.length > 0) {
6442
7014
  const summary = timer.time(
6443
7015
  "summarize",
6444
- () => (0, import_extractor8.assembleSummary)(
6445
- (0, import_extractor8.moduleInitStructure)({
7016
+ () => (0, import_extractor10.assembleSummary)(
7017
+ (0, import_extractor10.moduleInitStructure)({
6446
7018
  name: import_node_path3.default.basename(displayPath),
6447
7019
  file: displayPath,
6448
7020
  range: rangeOf(root),
@@ -6485,7 +7057,7 @@ async function extractPythonProject(options) {
6485
7057
  }
6486
7058
  summaries.push(...reached.summaries);
6487
7059
  const resolvedImports = resolvedImportsOf(db, displayPathOf);
6488
- (0, import_extractor8.stampModuleImports)(summaries, (file) => resolvedImports.get(file) ?? []);
7060
+ (0, import_extractor10.stampModuleImports)(summaries, (file) => resolvedImports.get(file) ?? []);
6489
7061
  const idRoot = options.projectRoot ?? options.workspaceRoot;
6490
7062
  for (const summary of summaries) {
6491
7063
  const absoluteFile = options.workspaceRoot === void 0 ? summary.location.file : import_node_path3.default.resolve(options.workspaceRoot, summary.location.file);
@@ -6498,12 +7070,16 @@ async function extractPythonProject(options) {
6498
7070
  }
6499
7071
  (0, import_behavioral_ir4.disambiguateSummaryIds)(summaries);
6500
7072
  (0, import_behavioral_ir4.linkCallsToSummaries)(summaries);
7073
+ const composed = timer.time(
7074
+ "summarize",
7075
+ () => (0, import_extractor10.composeWrappers)(summaries, { gapHandling })
7076
+ );
6501
7077
  await timer.timeAsync("cache.write", async () => {
6502
- if (cacheDir === null || summaries.length === 0) {
7078
+ if (cacheDir === null || composed.length === 0) {
6503
7079
  return;
6504
7080
  }
6505
7081
  try {
6506
- await cache.write(cacheInput, summaries);
7082
+ await cache.write(cacheInput, composed);
6507
7083
  } catch {
6508
7084
  }
6509
7085
  });
@@ -6512,11 +7088,11 @@ async function extractPythonProject(options) {
6512
7088
  packs: options.packs,
6513
7089
  tallies,
6514
7090
  filesWalked: options.files.length,
6515
- summaries
7091
+ summaries: composed
6516
7092
  })
6517
7093
  );
6518
7094
  options.onTiming?.(timer.report());
6519
- return { summaries, facts: db };
7095
+ return { summaries: composed, facts: db };
6520
7096
  }
6521
7097
  function resolvedImportsOf(db, displayPathOf) {
6522
7098
  const importsByFile = /* @__PURE__ */ new Map();