@enricai/barnacle 1.12.30 → 1.12.32

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.
@@ -2905,7 +2905,15 @@ function pathToFoldAccessorExpr(expr, path, depth = 0) {
2905
2905
  * can't collide either. Returns the loop's opening/closing lines (for the
2906
2906
  * caller to splice its own loop-body lines between) plus the ordered
2907
2907
  * (outer-to-inner) ancestor variable names — NOT including the innermost
2908
- * item, which the caller names itself via `itemVar`.
2908
+ * item, which the caller names itself via `itemVar`. Also splits the
2909
+ * combined open/close into an ancestor-scope segment
2910
+ * (`ancestorOpenLines`/`ancestorCloseLines`, empty when `path` crosses no
2911
+ * wildcard) and an item-scope segment (`itemOpenLines`/`itemCloseLines`),
2912
+ * so a caller whose per-target chain fetch never reads `itemVar` can splice
2913
+ * that fetch between the two segments — once per ancestor tuple — instead
2914
+ * of inside the item loop. `openLines`/`closeLines` remain the straight
2915
+ * concatenation of the two segments, unchanged, for callers that don't need
2916
+ * the split.
2909
2917
  */
2910
2918
  function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth = 0) {
2911
2919
  const wildcardIndex = path.indexOf(ARRAY_WILDCARD_SEGMENT);
@@ -2917,21 +2925,21 @@ function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth
2917
2925
  // single-scope folds must emit byte-identical code, not merely
2918
2926
  // equivalent code, since nothing about a flat fold needs the nested-loop
2919
2927
  // rewrite in the first place.
2920
- if (depth === 0) {
2921
- const foldItemsVar = `foldItems${varSuffix}`;
2922
- return {
2923
- openLines: [
2924
- `${indent}const ${foldItemsVar} = ${finalExpr};`,
2925
- `${indent}for (const ${itemVar} of ${foldItemsVar}) {`,
2926
- ],
2927
- closeLines: [`${indent}}`],
2928
- ancestorVars: [],
2929
- };
2930
- }
2928
+ const itemOpenLines = depth === 0
2929
+ ? [
2930
+ `${indent}const foldItems${varSuffix} = ${finalExpr};`,
2931
+ `${indent}for (const ${itemVar} of foldItems${varSuffix}) {`,
2932
+ ]
2933
+ : [`${indent}for (const ${itemVar} of ${finalExpr}) {`];
2934
+ const itemCloseLines = [`${indent}}`];
2931
2935
  return {
2932
- openLines: [`${indent}for (const ${itemVar} of ${finalExpr}) {`],
2933
- closeLines: [`${indent}}`],
2936
+ openLines: itemOpenLines,
2937
+ closeLines: itemCloseLines,
2934
2938
  ancestorVars: [],
2939
+ ancestorOpenLines: [],
2940
+ itemOpenLines,
2941
+ itemCloseLines,
2942
+ ancestorCloseLines: [],
2935
2943
  };
2936
2944
  }
2937
2945
  const before = path.slice(0, wildcardIndex);
@@ -2939,10 +2947,19 @@ function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth
2939
2947
  const groupVar = `g${depth}${varSuffix}`;
2940
2948
  const outerExpr = `${expr}${pathToAccessor(before, { assertNonNull: false })}`;
2941
2949
  const inner = pathToFoldLoopLines(groupVar, after, itemVar, `${indent} `, varSuffix, depth + 1);
2950
+ const ancestorOpenLines = [
2951
+ `${indent}for (const ${groupVar} of ${outerExpr}) {`,
2952
+ ...inner.ancestorOpenLines,
2953
+ ];
2954
+ const ancestorCloseLines = [...inner.ancestorCloseLines, `${indent}}`];
2942
2955
  return {
2943
- openLines: [`${indent}for (const ${groupVar} of ${outerExpr}) {`, ...inner.openLines],
2944
- closeLines: [...inner.closeLines, `${indent}}`],
2956
+ openLines: [...ancestorOpenLines, ...inner.itemOpenLines],
2957
+ closeLines: [...inner.itemCloseLines, ...ancestorCloseLines],
2945
2958
  ancestorVars: [groupVar, ...inner.ancestorVars],
2959
+ ancestorOpenLines,
2960
+ itemOpenLines: inner.itemOpenLines,
2961
+ itemCloseLines: inner.itemCloseLines,
2962
+ ancestorCloseLines,
2946
2963
  };
2947
2964
  }
2948
2965
  /** Suggests a JS-camelCase variable name for a state value path. Falls back
@@ -3743,7 +3760,7 @@ function emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinA
3743
3760
  if (target.chainArrayPath.length === 0) {
3744
3761
  return [
3745
3762
  ` const foldMatch${suffix} = ${terminalStep.varName} as Record<string, unknown>;`,
3746
- ` Object.assign(${itemVar}, foldMatch${suffix} ?? {});`,
3763
+ ` Object.assign(${itemVar}, Object.fromEntries(Object.entries(foldMatch${suffix} ?? {}).filter(([k]) => !(k in ${itemVar}))));`,
3747
3764
  ];
3748
3765
  }
3749
3766
  const foldMatchesExpr = pathToFoldAccessorExpr(`(${terminalStep.varName} as ${foldArrayAssertionType(target.chainArrayPath)})`, target.chainArrayPath);
@@ -3781,7 +3798,7 @@ function emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinA
3781
3798
  return [
3782
3799
  ` const foldMatches${suffix} = ${foldMatchesExpr};`,
3783
3800
  ` const foldMatch${suffix} = foldMatches${suffix}.length === 1 && ${soleCandidateFieldsAbsent} ? foldMatches${suffix}[0] : foldMatches${suffix}.find((m) => ${joinCondition});`,
3784
- ` Object.assign(${itemVar}, foldMatch${suffix} ?? {});`,
3801
+ ` Object.assign(${itemVar}, Object.fromEntries(Object.entries(foldMatch${suffix} ?? {}).filter(([k]) => !(k in ${itemVar}))));`,
3785
3802
  ];
3786
3803
  }
3787
3804
  /** Exported for unit testing — lets tests drive the multipart-upload code path directly
@@ -4268,8 +4285,16 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
4268
4285
  // Nested `for` loops (not a `.flatMap`-derived collection) so every
4269
4286
  // intermediate array's binding stays addressable inside the innermost
4270
4287
  // loop body — see pathToFoldLoopLines's docstring.
4271
- const { openLines, closeLines, ancestorVars } = pathToFoldLoopLines(`(${primaryStep.varName} as ${primaryArrType})`, foldPlan.primaryArrayPath, itemVar, " ", planSuffix);
4272
- lines.push(...openLines);
4288
+ const { ancestorOpenLines, itemOpenLines, itemCloseLines, ancestorCloseLines, ancestorVars } = pathToFoldLoopLines(`(${primaryStep.varName} as ${primaryArrType})`, foldPlan.primaryArrayPath, itemVar, " ", planSuffix);
4289
+ lines.push(...ancestorOpenLines);
4290
+ // Chain fetches for targets whose params + joinFields never reference
4291
+ // itemVar are spliced here, above the item loop but inside the
4292
+ // ancestor loop(s) — fetched once per ancestor tuple and reused by
4293
+ // every descendant item's join/merge, instead of once per item.
4294
+ const hoistedChainLines = [];
4295
+ // Every target's join/merge — plus any non-hoistable target's own
4296
+ // chain fetch — goes here, spliced inside the item loop.
4297
+ const itemScopedLines = [];
4273
4298
  for (const [targetIndex, target] of foldPlan.targets.entries()) {
4274
4299
  // `firstItem` decides which captured literal `parameterize` rewrites
4275
4300
  // — it must be the item at `primaryMatchedItemIndex`, the one THIS
@@ -4323,7 +4348,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
4323
4348
  // invisible and gets frozen as a literal.
4324
4349
  const threadedFields = dedupeThreadedFields([
4325
4350
  ...target.joinFields.map((field) => ({ varName: itemVar, field })),
4326
- ...findThreadedJoinFields(threadingScopes, chainCapture),
4351
+ ...findThreadedJoinFields(threadingScopes, chainCapture, actions.map((a) => a.capture)),
4327
4352
  ]);
4328
4353
  const result = threadedFields.reduce((acc, { varName, field }) => {
4329
4354
  const replacement = `\${${scopedAccessor(varName, field)}}`;
@@ -4366,19 +4391,33 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
4366
4391
  // templates by the pass above, same as it would be for any two
4367
4392
  // sequential non-fold steps) resolves them from this narrower scope.
4368
4393
  const chainDeclared = new Set();
4394
+ const chainLines = [];
4395
+ // True once any chain step's parameterized url/headers/body actually
4396
+ // ends up referencing `itemVar` — determined off the SAME
4397
+ // `parameterize` substitutions the emitted call itself uses (not a
4398
+ // fresh value scan), so this can never disagree with what the
4399
+ // request literally interpolates. `ancestorVars.length === 0` means
4400
+ // there is no ancestor loop to hoist into in the first place (a
4401
+ // flat, single-level fold), so it is treated as item-scoped too —
4402
+ // existing flat folds must keep emitting byte-identical code.
4403
+ const itemVarRefPattern = new RegExp(`\\$\\{${itemVar}[.[]`);
4404
+ let referencesItemVar = ancestorVars.length === 0;
4369
4405
  for (const chainIndex of target.chain) {
4370
4406
  const chainStep = actions[chainIndex];
4371
4407
  const chainRendered = rendered[chainIndex];
4372
- lines.push(` const ${chainStep.varName} = (await httpClient(\`${parameterize(chainRendered.url, chainStep.capture)}\`, {`, ` method: ${JSON.stringify(chainRendered.method)},`);
4373
- const joined = [
4374
- parameterize(chainRendered.headersExpr, chainStep.capture),
4375
- parameterize(chainRendered.bodyArg, chainStep.capture),
4376
- ]
4377
- .filter((s) => s !== "")
4378
- .join(" ");
4408
+ const paramUrl = parameterize(chainRendered.url, chainStep.capture);
4409
+ const paramHeaders = parameterize(chainRendered.headersExpr, chainStep.capture);
4410
+ const paramBody = parameterize(chainRendered.bodyArg, chainStep.capture);
4411
+ if (itemVarRefPattern.test(paramUrl) ||
4412
+ itemVarRefPattern.test(paramHeaders) ||
4413
+ itemVarRefPattern.test(paramBody)) {
4414
+ referencesItemVar = true;
4415
+ }
4416
+ chainLines.push(` const ${chainStep.varName} = (await httpClient(\`${paramUrl}\`, {`, ` method: ${JSON.stringify(chainRendered.method)},`);
4417
+ const joined = [paramHeaders, paramBody].filter((s) => s !== "").join(" ");
4379
4418
  if (joined !== "")
4380
- lines.push(` ${joined}`);
4381
- lines.push(` schema: ${chainRendered.schemaExpr},`, ` })) as Record<string, unknown>;`);
4419
+ chainLines.push(` ${joined}`);
4420
+ chainLines.push(` schema: ${chainRendered.schemaExpr},`, ` })) as Record<string, unknown>;`);
4382
4421
  for (const p of chainStep.produces) {
4383
4422
  if (p.kind === "header")
4384
4423
  continue;
@@ -4388,13 +4427,20 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
4388
4427
  continue;
4389
4428
  chainDeclared.add(p.name);
4390
4429
  const assertion = pathToAssertionType(p.path);
4391
- lines.push(` const ${p.name} = (${chainStep.varName} as ${assertion})${pathToAccessor(p.path, { assertNonNull: false })};`);
4430
+ chainLines.push(` const ${p.name} = (${chainStep.varName} as ${assertion})${pathToAccessor(p.path, { assertNonNull: false })};`);
4392
4431
  }
4393
4432
  }
4394
4433
  const terminalStep = actions[target.chainTerminalIndex];
4395
- lines.push(...emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor));
4434
+ const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
4435
+ if (referencesItemVar) {
4436
+ itemScopedLines.push(...chainLines, ...matchLines);
4437
+ }
4438
+ else {
4439
+ hoistedChainLines.push(...chainLines);
4440
+ itemScopedLines.push(...matchLines);
4441
+ }
4396
4442
  }
4397
- lines.push(...closeLines, "");
4443
+ lines.push(...hoistedChainLines, ...itemOpenLines, ...itemScopedLines, ...itemCloseLines, ...ancestorCloseLines, "");
4398
4444
  continue;
4399
4445
  }
4400
4446
  // Every other chain step (already fully emitted, inline, by the fold
@@ -4813,21 +4859,53 @@ function findObjectArrayFieldOrWholeObject(value, path = []) {
4813
4859
  * path segment (e.g. `/orders/{id}`) rather than a query param or body
4814
4860
  * field. Numeric leaves are included because a join key is just as often a
4815
4861
  * numeric id (threaded as a query param string or a JSON body number
4816
- * literal) as a string one. */
4817
- function collectRequestStringValues(capture) {
4862
+ * literal) as a string one.
4863
+ *
4864
+ * `allCaptures`, when passed, gates every query param and body leaf on
4865
+ * whether its OWN value (by param key / body path, via {@link endpointKey}'s
4866
+ * same-endpoint grouping — the same grouping {@link
4867
+ * findFrozenVaryingDrillParams} uses) ever differs on some other capture of
4868
+ * the same endpoint. A value that never varies (e.g. `adults=2`) is left
4869
+ * out, so callers matching by pure value equality (e.g. {@link
4870
+ * findThreadedJoinFields}) can't bind that constant onto an unrelated field
4871
+ * that coincidentally holds the same literal (e.g. a zero-valued `children`
4872
+ * param matching a discount amount that also happens to be `0` in one
4873
+ * capture, then diverges to a fraction in another). Path segments are
4874
+ * exempt from this gate and always pass through — {@link endpointKey}
4875
+ * already fixes the pathname, so a path segment can never be observed to
4876
+ * vary within a matched group, the same rationale {@link
4877
+ * findFrozenVaryingDrillParams} documents for the same exclusion. When no
4878
+ * other capture of this endpoint exists in `allCaptures`, variance can't be
4879
+ * observed either way, so every value is kept (unfiltered, matching the
4880
+ * behavior when `allCaptures` is omitted). */
4881
+ function collectRequestStringValues(capture, allCaptures) {
4882
+ const sameEndpointCaptures = allCaptures
4883
+ ? allCaptures.filter((c) => c !== capture && endpointKey(c.url) === endpointKey(capture.url))
4884
+ : [];
4885
+ const varies = (own, others) => !allCaptures || sameEndpointCaptures.length === 0
4886
+ ? true
4887
+ : others.some((other) => other !== undefined && other !== own);
4818
4888
  const values = new Set();
4819
4889
  try {
4820
4890
  const url = new URL(capture.url);
4821
- for (const v of url.searchParams.values())
4822
- values.add(v);
4823
4891
  for (const segment of url.pathname.split("/").filter(Boolean))
4824
4892
  values.add(segment);
4893
+ for (const [paramKey, value] of url.searchParams.entries()) {
4894
+ const otherValues = sameEndpointCaptures.map((c) => {
4895
+ try {
4896
+ return new URL(c.url).searchParams.get(paramKey) ?? undefined;
4897
+ }
4898
+ catch {
4899
+ return undefined;
4900
+ }
4901
+ });
4902
+ if (varies(value, otherValues))
4903
+ values.add(value);
4904
+ }
4825
4905
  }
4826
4906
  catch {
4827
4907
  // Relative or malformed URL — no query params or path segments to contribute.
4828
4908
  }
4829
- for (const v of jsonBodyLeafValues(capture.requestPostData) ?? [])
4830
- values.add(v);
4831
4909
  const parsedBody = (() => {
4832
4910
  try {
4833
4911
  return typeof capture.requestPostData === "string" && capture.requestPostData.length > 0
@@ -4839,9 +4917,26 @@ function collectRequestStringValues(capture) {
4839
4917
  }
4840
4918
  })();
4841
4919
  if (parsedBody !== undefined) {
4842
- for (const { value } of walkAllPrimitiveLeaves(parsedBody)) {
4843
- if (typeof value === "number" || typeof value === "boolean")
4844
- values.add(String(value));
4920
+ for (const { path, value } of walkAllPrimitiveLeaves(parsedBody)) {
4921
+ if (value === null)
4922
+ continue;
4923
+ const stringValue = String(value);
4924
+ const otherValues = sameEndpointCaptures.map((c) => {
4925
+ try {
4926
+ const otherBody = typeof c.requestPostData === "string" && c.requestPostData.length > 0
4927
+ ? JSON.parse(c.requestPostData)
4928
+ : undefined;
4929
+ if (otherBody === undefined)
4930
+ return undefined;
4931
+ const otherValue = readValueAtPath(otherBody, path);
4932
+ return otherValue === undefined ? undefined : String(otherValue);
4933
+ }
4934
+ catch {
4935
+ return undefined;
4936
+ }
4937
+ });
4938
+ if (varies(stringValue, otherValues))
4939
+ values.add(stringValue);
4845
4940
  }
4846
4941
  }
4847
4942
  return values;
@@ -5061,9 +5156,19 @@ function dedupeThreadedFields(fields) {
5061
5156
  * depends on. `scopes` is searched in the given order (innermost fold item
5062
5157
  * first, then each ancestor binding a nested loop keeps addressable), since
5063
5158
  * an ancestor field with the same name as an item field must not shadow the
5064
- * item's own value. */
5065
- function findThreadedJoinFields(scopes, drillCapture) {
5066
- const requestValues = collectRequestStringValues(drillCapture);
5159
+ * item's own value.
5160
+ *
5161
+ * `allCaptures`, when passed, is forwarded to {@link
5162
+ * collectRequestStringValues} to gate matching on cross-capture variance:
5163
+ * a param whose own value never varies across other captures of the same
5164
+ * endpoint is excluded from the candidate set entirely, so a constant like
5165
+ * `children=0` can't bind to an unrelated item field (a discount amount, a
5166
+ * departure port) purely because both happen to equal the same literal at
5167
+ * generation time. Omitted for the candidate-array disambiguation callers,
5168
+ * where narrowing by variance is a different concern than the URL/body
5169
+ * over-threading this gate exists to prevent. */
5170
+ function findThreadedJoinFields(scopes, drillCapture, allCaptures) {
5171
+ const requestValues = collectRequestStringValues(drillCapture, allCaptures);
5067
5172
  if (requestValues.size === 0)
5068
5173
  return [];
5069
5174
  return scopes.flatMap(({ varName, obj }) => [...walkItemFieldPaths(obj)]
@@ -6300,7 +6405,7 @@ function foldResponseBodyForShapeInference(actionSteps, foldPlan, initialBody =
6300
6405
  String(readValueAtPath(matchedItem, f.split("."))))) ?? drillItems?.[0];
6301
6406
  if (!primaryItems || !matchedItem || !drillMatch)
6302
6407
  return body;
6303
- return replaceByReference(body, matchedItem, { ...matchedItem, ...drillMatch });
6408
+ return replaceByReference(body, matchedItem, { ...drillMatch, ...matchedItem });
6304
6409
  }, initialBody);
6305
6410
  }
6306
6411
  /**
@@ -6903,7 +7008,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
6903
7008
  return foldPlan.primaryArrayPath.slice(level.length);
6904
7009
  })()
6905
7010
  : foldPlan.primaryArrayPath;
6906
- const { openLines, closeLines, ancestorVars } = itemsOverride
7011
+ const { ancestorOpenLines, itemOpenLines, itemCloseLines, ancestorCloseLines, ancestorVars } = itemsOverride
6907
7012
  ? pathToFoldLoopLines(residualPath.length === 0
6908
7013
  ? itemsOverride.expr
6909
7014
  : `(${itemsOverride.expr} as ${pathAccessTypeExpr(`${pascal}Response`, itemsOverride.level)}[number][])`, residualPath, itemVar, " ", planSuffix)
@@ -6914,7 +7019,17 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
6914
7019
  // crossings correspond to `ancestorVars` above, so slice the
6915
7020
  // design-time ancestor chain to match.
6916
7021
  const residualAncestorCount = residualPath.filter((s) => s === ARRAY_WILDCARD_SEGMENT).length;
6917
- lines.push(...openLines);
7022
+ lines.push(...ancestorOpenLines);
7023
+ // Chain fetches for targets whose parameterized URL never interpolates
7024
+ // `itemVar` are spliced here, above the item loop but inside the
7025
+ // ancestor loop(s) — fetched once per ancestor tuple and reused by
7026
+ // every descendant item's join/merge, mirroring
7027
+ // emitMultiStepExecuteHttp's identical hoist (see its own comment).
7028
+ const hoistedChainLines = [];
7029
+ // Every target's join/merge — plus any non-hoistable target's own
7030
+ // chain fetch — goes here, spliced inside the item loop.
7031
+ const itemScopedLines = [];
7032
+ const itemVarRefPattern = new RegExp(`\\$\\{${itemVar}[.[]`);
6918
7033
  for (const [targetIndex, target] of foldPlan.targets.entries()) {
6919
7034
  const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
6920
7035
  if (!matchedPrimaryItem) {
@@ -6953,7 +7068,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
6953
7068
  : rawUrl;
6954
7069
  const threadedFields = dedupeThreadedFields([
6955
7070
  ...target.joinFields.map((field) => ({ varName: itemVar, field })),
6956
- ...findThreadedJoinFields(threadingScopes, chainCapture),
7071
+ ...findThreadedJoinFields(threadingScopes, chainCapture, actionSteps.map((s) => s.capture)),
6957
7072
  ]);
6958
7073
  const result = threadedFields.reduce((acc, { varName, field }) => {
6959
7074
  const scopeObj = varName === itemVar ? firstItem : ancestorObjByVar.get(varName);
@@ -6970,25 +7085,44 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
6970
7085
  assertNoFrozenVaryingDrillParams("emitContractTs", chainCapture, result, actionSteps.map((s) => s.capture));
6971
7086
  return result;
6972
7087
  };
7088
+ const chainLines = [];
7089
+ // True once any chain step's parameterized URL actually ends up
7090
+ // referencing `itemVar`, off the SAME `parameterizeUrl` call the
7091
+ // request literally interpolates — see emitMultiStepExecuteHttp's
7092
+ // identical `referencesItemVar` for why this can't disagree with
7093
+ // what's emitted. `ancestorVars.length === 0` means there's no
7094
+ // ancestor loop to hoist into (a flat, single-level fold), so it's
7095
+ // treated as item-scoped too — flat folds must keep emitting
7096
+ // byte-identical code.
7097
+ let referencesItemVar = ancestorVars.length === 0;
6973
7098
  for (const chainIndex of target.chain) {
6974
7099
  const chainStep = actionSteps[chainIndex];
6975
7100
  if (!chainStep)
6976
7101
  continue;
6977
7102
  const url = parameterizeUrl(chainStep.capture.url, chainStep.capture);
7103
+ if (itemVarRefPattern.test(url))
7104
+ referencesItemVar = true;
6978
7105
  const schemaExpr = inferZodSchema(chainStep.capture.responseBody, 0, "", {
6979
7106
  looseServerResponse: true,
6980
7107
  aggregateUnitBasisFindingsByPath: groupAggregateUnitBasisFindingsByPath([
6981
7108
  chainStep.capture.responseBody,
6982
7109
  ]),
6983
7110
  });
6984
- lines.push(` const ${chainStep.varName} = (await httpClient(\`${url}\`, {`, ` method: ${JSON.stringify(chainStep.capture.method)},`, ` schema: ${schemaExpr},`, ` })) as Record<string, unknown>;`);
7111
+ chainLines.push(` const ${chainStep.varName} = (await httpClient(\`${url}\`, {`, ` method: ${JSON.stringify(chainStep.capture.method)},`, ` schema: ${schemaExpr},`, ` })) as Record<string, unknown>;`);
6985
7112
  }
6986
7113
  const terminalStep = actionSteps[target.chainTerminalIndex];
6987
7114
  if (!terminalStep)
6988
7115
  continue;
6989
- lines.push(...emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor));
7116
+ const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
7117
+ if (referencesItemVar) {
7118
+ itemScopedLines.push(...chainLines, ...matchLines);
7119
+ }
7120
+ else {
7121
+ hoistedChainLines.push(...chainLines);
7122
+ itemScopedLines.push(...matchLines);
7123
+ }
6990
7124
  }
6991
- lines.push(...closeLines, "");
7125
+ lines.push(...hoistedChainLines, ...itemOpenLines, ...itemScopedLines, ...itemCloseLines, ...ancestorCloseLines, "");
6992
7126
  }
6993
7127
  return lines;
6994
7128
  };