@enricai/barnacle 1.12.52 → 1.12.55

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.
Files changed (70) hide show
  1. package/dist/api/schemas/common.d.ts +28 -0
  2. package/dist/api/schemas/common.d.ts.map +1 -1
  3. package/dist/api/schemas/common.js +51 -0
  4. package/dist/api/schemas/common.js.map +1 -1
  5. package/dist/plugins/config-plugin.d.ts +8 -0
  6. package/dist/plugins/config-plugin.d.ts.map +1 -1
  7. package/dist/plugins/config-plugin.js +56 -3
  8. package/dist/plugins/config-plugin.js.map +1 -1
  9. package/dist/plugins/json-schema-to-zod.d.ts +6 -3
  10. package/dist/plugins/json-schema-to-zod.d.ts.map +1 -1
  11. package/dist/plugins/json-schema-to-zod.js +18 -6
  12. package/dist/plugins/json-schema-to-zod.js.map +1 -1
  13. package/dist/plugins/loader.d.ts.map +1 -1
  14. package/dist/plugins/loader.js +7 -1
  15. package/dist/plugins/loader.js.map +1 -1
  16. package/dist/recon/capture-filters.d.ts +2 -55
  17. package/dist/recon/capture-filters.d.ts.map +1 -1
  18. package/dist/recon/capture-filters.js +152 -4
  19. package/dist/recon/capture-filters.js.map +1 -1
  20. package/dist/recon/load-value-constraints.d.ts +22 -0
  21. package/dist/recon/load-value-constraints.d.ts.map +1 -0
  22. package/dist/recon/load-value-constraints.js +61 -0
  23. package/dist/recon/load-value-constraints.js.map +1 -0
  24. package/dist/recon/value-constraints.d.ts +46 -0
  25. package/dist/recon/value-constraints.d.ts.map +1 -0
  26. package/dist/recon/value-constraints.js +31 -0
  27. package/dist/recon/value-constraints.js.map +1 -0
  28. package/dist/scraper/errors.d.ts +12 -0
  29. package/dist/scraper/errors.d.ts.map +1 -1
  30. package/dist/scraper/errors.js +21 -1
  31. package/dist/scraper/errors.js.map +1 -1
  32. package/dist/scraper/flow-runner.d.ts +87 -23
  33. package/dist/scraper/flow-runner.d.ts.map +1 -1
  34. package/dist/scraper/flow-runner.js +417 -71
  35. package/dist/scraper/flow-runner.js.map +1 -1
  36. package/dist/scraper/http-client.d.ts +11 -0
  37. package/dist/scraper/http-client.d.ts.map +1 -1
  38. package/dist/scraper/http-client.js +96 -2
  39. package/dist/scraper/http-client.js.map +1 -1
  40. package/dist/scraper/phantom-click.d.ts +2 -2
  41. package/dist/scraper/phantom-click.d.ts.map +1 -1
  42. package/dist/scraper/phantom-click.js +6 -5
  43. package/dist/scraper/phantom-click.js.map +1 -1
  44. package/dist/scraper/submit-control.d.ts +11 -6
  45. package/dist/scraper/submit-control.d.ts.map +1 -1
  46. package/dist/scraper/submit-control.js +38 -7
  47. package/dist/scraper/submit-control.js.map +1 -1
  48. package/dist/scripts/recon-browser.d.ts +45 -8
  49. package/dist/scripts/recon-browser.d.ts.map +1 -1
  50. package/dist/scripts/recon-browser.js +145 -16
  51. package/dist/scripts/recon-browser.js.map +1 -1
  52. package/dist/scripts/recon-generate-multicall-fixture.d.ts +156 -0
  53. package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
  54. package/dist/scripts/recon-generate-multicall-fixture.js +489 -0
  55. package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
  56. package/dist/scripts/recon-generate.d.ts +65 -4
  57. package/dist/scripts/recon-generate.d.ts.map +1 -1
  58. package/dist/scripts/recon-generate.js +514 -110
  59. package/dist/scripts/recon-generate.js.map +1 -1
  60. package/dist/scripts/recon-summarize.d.ts +11 -1
  61. package/dist/scripts/recon-summarize.d.ts.map +1 -1
  62. package/dist/scripts/recon-summarize.js +51 -32
  63. package/dist/scripts/recon-summarize.js.map +1 -1
  64. package/dist/scripts/smoke-test.d.ts +20 -1
  65. package/dist/scripts/smoke-test.d.ts.map +1 -1
  66. package/dist/scripts/smoke-test.js +13 -4
  67. package/dist/scripts/smoke-test.js.map +1 -1
  68. package/dist/site-plugin.d.ts +9 -0
  69. package/dist/site-plugin.d.ts.map +1 -1
  70. package/package.json +1 -1
@@ -66,6 +66,7 @@ exports.getScanPrimaryCandidateGroupsCallCountForTest = getScanPrimaryCandidateG
66
66
  exports.resetScanPrimaryCandidateGroupsCallCountForTest = resetScanPrimaryCandidateGroupsCallCountForTest;
67
67
  exports.detectDrillDownFoldPlan = detectDrillDownFoldPlan;
68
68
  exports.parseFoldReturnSpec = parseFoldReturnSpec;
69
+ exports.parseFallbackGateSpec = parseFallbackGateSpec;
69
70
  exports.collectRequestValuesIncludingHeaders = collectRequestValuesIncludingHeaders;
70
71
  exports.getFoldPlanResolutionCallCountForTests = getFoldPlanResolutionCallCountForTests;
71
72
  exports.resetFoldPlanResolutionCallCountForTests = resetFoldPlanResolutionCallCountForTests;
@@ -89,7 +90,9 @@ const plugin_api_version_1 = require("../plugins/plugin-api-version");
89
90
  const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
90
91
  const capture_filters_1 = require("../recon/capture-filters");
91
92
  const load_form_schema_1 = require("../recon/load-form-schema");
93
+ const load_value_constraints_1 = require("../recon/load-value-constraints");
92
94
  const load_vocabulary_1 = require("../recon/load-vocabulary");
95
+ const value_constraints_1 = require("../recon/value-constraints");
93
96
  const vocabulary_1 = require("../recon/vocabulary");
94
97
  const recon_shared_1 = require("./recon-shared");
95
98
  const logger = (0, logging_1.getScriptLogger)("recon-generate");
@@ -1798,6 +1801,17 @@ function collapseRedundantPatches(actions) {
1798
1801
  * counter, unpaired with an explicit page-size key) since that is the common
1799
1802
  * REST shape, unlike GraphQL's paired variables convention. */
1800
1803
  const PAGINATION_FIELD_NAME_PATTERN = /^(page|pagenum|pagenumber|pageindex|pageno|offset|skip|start|cursor)$/i;
1804
+ /** Response leaf-key shapes that name a discoverable ceiling on some other
1805
+ * numeric quantity (a capacity, a maximum, a limit) -- structural, like
1806
+ * {@link PAGINATION_FIELD_NAME_PATTERN}, not tied to any domain's field
1807
+ * vocabulary. Used to bound a request-side numeric field whose observed
1808
+ * values never exceed a same-run response's declared ceiling. */
1809
+ const CAPACITY_FIELD_NAME_PATTERN = /(max|capacity|limit)/i;
1810
+ /** Upper bound on how many distinct string values a request field may show
1811
+ * across a run's captures before it stops looking like a closed-set facet
1812
+ * vocabulary and starts looking like free text -- past this, emitting
1813
+ * z.enum(...) would lock callers out of values recon simply never sampled. */
1814
+ const VOCABULARY_ENUM_MAX_DISTINCT_VALUES = 8;
1801
1815
  /** Request-field key names that name known client-generated scaffolding
1802
1816
  * (a monotonic sequence counter, a correlation/trace id, an idempotency
1803
1817
  * nonce) rather than genuine payload data. Gates {@link
@@ -3928,6 +3942,41 @@ function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth
3928
3942
  ancestorCloseLines,
3929
3943
  };
3930
3944
  }
3945
+ /**
3946
+ * Splices a fold's per-item body between `itemOpenLines`/`itemCloseLines`
3947
+ * (see {@link pathToFoldLoopLines}) — either as the original sequential
3948
+ * `for` loop, when the body never issues its own per-item drill fetch (so
3949
+ * existing byte-identical fold output is preserved for every fold that has
3950
+ * nothing to parallelize), or rewritten into a `Promise.allSettled`-based
3951
+ * parallel form when it does: `for (const item of X) {` becomes `await
3952
+ * Promise.allSettled((X).map(async (item) => { ... }));`. Every item's
3953
+ * fetch then fires concurrently instead of one at a time, and a rejected
3954
+ * item's promise can't abort a sibling's — `allSettled` never short-
3955
+ * circuits on a rejection, and the sibling's `Object.assign` onto its own
3956
+ * item already only runs once ITS OWN fetch resolves, so a failed item
3957
+ * simply keeps its original (unmerged) fields instead of taking every other
3958
+ * item's already-fetched data down with it.
3959
+ */
3960
+ function emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch) {
3961
+ if (!hasItemScopedFetch) {
3962
+ return [...itemOpenLines, ...itemScopedLines, ...itemCloseLines];
3963
+ }
3964
+ const forLineIndex = itemOpenLines.length - 1;
3965
+ const forLine = itemOpenLines[forLineIndex];
3966
+ const forLineMatch = forLine.match(/^(\s*)for \(const \w+ of (.+)\) \{$/);
3967
+ if (!forLineMatch) {
3968
+ throw new Error(`emitItemLoopLines: unrecognized fold item-loop shape: ${forLine}`);
3969
+ }
3970
+ const [, indent, iterableExpr] = forLineMatch;
3971
+ return [
3972
+ ...itemOpenLines.slice(0, forLineIndex),
3973
+ `${indent}await Promise.allSettled(`,
3974
+ `${indent} (${iterableExpr}).map(async (${itemVar}) => {`,
3975
+ ...itemScopedLines,
3976
+ `${indent} })`,
3977
+ `${indent});`,
3978
+ ];
3979
+ }
3931
3980
  /** Suggests a JS-camelCase variable name for a state value path. Falls back
3932
3981
  * up the path if the tail is numeric or not a valid JS identifier. */
3933
3982
  function pathToVarName(path) {
@@ -4817,10 +4866,15 @@ function applyUrlParamPayloadSubstitutions(template, parsedBody, bindings) {
4817
4866
  * even though the field genuinely IS one this step's own request sends as
4818
4867
  * caller-supplied data.
4819
4868
  */
4820
- function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies = [], outAdditionalKeys = new Map()) {
4869
+ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies = [], outAdditionalKeys = new Map(),
4870
+ /** Every capture's response body from the same run, scanned only for
4871
+ * {@link CAPACITY_FIELD_NAME_PATTERN}-shaped numeric leaves — the
4872
+ * evidence source for a discovered "number" key's `capacityMax`. */
4873
+ responseBodiesForCapacitySignal = []) {
4821
4874
  const merged = [];
4822
4875
  const seenPairs = new Set();
4823
4876
  const seenValueByKey = new Map();
4877
+ const distinctValuesByKey = new Map();
4824
4878
  const allBodies = [inputBody, ...additionalBodies];
4825
4879
  for (const body of allBodies) {
4826
4880
  if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
@@ -4854,6 +4908,17 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
4854
4908
  continue;
4855
4909
  }
4856
4910
  seenValueByKey.set(key, value);
4911
+ // Tracked independently of the (key, value) dedupe below — a facet's
4912
+ // vocabulary or a capacity ceiling needs every distinct value it took,
4913
+ // not just the deduped substitution list, and pagination cursors are
4914
+ // excluded from both (their values grow monotonically and are never a
4915
+ // closed set).
4916
+ if ((typeof value === "string" || typeof value === "number") &&
4917
+ !PAGINATION_FIELD_NAME_PATTERN.test(key)) {
4918
+ const distinct = distinctValuesByKey.get(key) ?? new Set();
4919
+ distinct.add(value);
4920
+ distinctValuesByKey.set(key, distinct);
4921
+ }
4857
4922
  const pairKey = `${key} ${typeof value} ${value}`;
4858
4923
  if (seenPairs.has(pairKey))
4859
4924
  continue;
@@ -4866,11 +4931,54 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
4866
4931
  // isReservedByApplicantContactSchema — this function has no visibility
4867
4932
  // into that flag, so it must not special-case inputBody's own keys.
4868
4933
  if (typeof value === "string")
4869
- outAdditionalKeys.set(key, "string");
4934
+ outAdditionalKeys.set(key, { kind: "string" });
4870
4935
  else if (typeof value === "number")
4871
- outAdditionalKeys.set(key, "number");
4936
+ outAdditionalKeys.set(key, { kind: "number" });
4872
4937
  else if (typeof value === "boolean")
4873
- outAdditionalKeys.set(key, "boolean");
4938
+ outAdditionalKeys.set(key, { kind: "boolean" });
4939
+ }
4940
+ }
4941
+ // Vocabulary-derived enum: a "string" key whose distinct observed values
4942
+ // form a small closed set is a facet field, not free text — emit the set
4943
+ // so the generated schema rejects a caller value recon never sampled.
4944
+ // Capacity-bounded number: a "number" key gets a ceiling only when some
4945
+ // same-run response carries a max/capacity/limit-shaped numeric leaf that
4946
+ // is >= every value this key was observed to carry; the tightest such
4947
+ // ceiling wins so the bound is never looser than the evidence supports.
4948
+ const capacityCandidatesByMinBound = () => {
4949
+ const candidates = [];
4950
+ for (const body of responseBodiesForCapacitySignal) {
4951
+ if (body === undefined || body === null || typeof body !== "object")
4952
+ continue;
4953
+ for (const { value, path } of walkAllPrimitiveLeaves(body)) {
4954
+ const leafName = path[path.length - 1] ?? "";
4955
+ if (typeof value !== "number")
4956
+ continue;
4957
+ if (!CAPACITY_FIELD_NAME_PATTERN.test(leafName))
4958
+ continue;
4959
+ candidates.push(value);
4960
+ }
4961
+ }
4962
+ return candidates;
4963
+ };
4964
+ const capacitySignals = capacityCandidatesByMinBound();
4965
+ for (const [key, info] of outAdditionalKeys) {
4966
+ const distinct = distinctValuesByKey.get(key);
4967
+ if (!distinct)
4968
+ continue;
4969
+ if (info.kind === "string") {
4970
+ if (distinct.size >= 2 && distinct.size <= VOCABULARY_ENUM_MAX_DISTINCT_VALUES) {
4971
+ info.enumValues = [...distinct].map(String).sort();
4972
+ }
4973
+ continue;
4974
+ }
4975
+ if (info.kind === "number") {
4976
+ const maxObserved = Math.max(...[...distinct].map(Number));
4977
+ const tightestCeiling = capacitySignals
4978
+ .filter((v) => v >= maxObserved)
4979
+ .sort((a, b) => a - b)[0];
4980
+ if (tightestCeiling !== undefined)
4981
+ info.capacityMax = tightestCeiling;
4874
4982
  }
4875
4983
  }
4876
4984
  let result = template;
@@ -5254,6 +5362,12 @@ pascalName = null) {
5254
5362
  // skip non-JSON bodies (e.g. multipart raw bytes)
5255
5363
  }
5256
5364
  }
5365
+ // Every action's response body, gathered once for the whole flow so a
5366
+ // capacity/max ceiling discovered on ANY capture (not just the one that
5367
+ // carries the request field itself) is available to bound a request-side
5368
+ // numeric field — the ceiling and the field it bounds are frequently
5369
+ // declared on different calls (an availability check, then a booking).
5370
+ const allResponseBodies = actions.map((a) => a.capture.responseBody);
5257
5371
  // Every value `applyPayloadKeyValueSubstitutions` will unconditionally
5258
5372
  // payload-ify from the ENTRY payload's own top-level key/value pairs,
5259
5373
  // gathered here so {@link interpolateStateValues} can give it payload
@@ -5457,7 +5571,7 @@ pascalName = null) {
5457
5571
  ? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
5458
5572
  : rawBodyWithProducerBoundary;
5459
5573
  const bodyAfterStateAndKv = rawBodyWithUrlParams
5460
- ? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
5574
+ ? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies)
5461
5575
  : "";
5462
5576
  // Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
5463
5577
  // rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
@@ -5662,6 +5776,15 @@ pascalName = null) {
5662
5776
  // Every target's join/merge — plus any non-hoistable target's own
5663
5777
  // chain fetch — goes here, spliced inside the item loop.
5664
5778
  const itemScopedLines = [];
5779
+ // True once any target's own chain fetch ends up item-scoped (not
5780
+ // hoisted above the item loop) — only then does the item loop have
5781
+ // anything to parallelize; see emitItemLoopLines.
5782
+ let hasItemScopedFetch = false;
5783
+ // True once any item-scoped target's chain threads a response header
5784
+ // through createHttpClient's shared `bind` store — see
5785
+ // chainUsesHeaderThreading below for why that disqualifies
5786
+ // parallelizing the item loop.
5787
+ let hasItemScopedHeaderThreading = false;
5665
5788
  for (const [targetIndex, target] of foldPlan.targets.entries()) {
5666
5789
  // `firstItem` decides which captured literal `parameterize` rewrites
5667
5790
  // — it must be the item at `primaryMatchedItemIndex`, the one THIS
@@ -5764,23 +5887,10 @@ pascalName = null) {
5764
5887
  // find it. Rebinding by structure, not value, lets referencesItemVar
5765
5888
  // (below) correctly stay false so the fetch hoists above the
5766
5889
  // per-item loop instead of pinning to it.
5767
- // Pairs each raw threaded field (whose VALUE is what's actually
5768
- // present in the captured text — the only thing a literal-value
5769
- // search can ever find) with the accessor it should render as.
5770
- // For an ancestor-scoped rebind these deliberately diverge: the
5771
- // literal search must still key off the item's own field (that's
5772
- // the value textually present), while the emitted accessor points
5773
- // at the ancestor's structurally-corresponding field instead —
5774
- // searching for the ANCESTOR field's (different) value would never
5775
- // match anything in `text` and silently freeze the literal.
5776
- const threadedFieldPairs = isAncestorScoped
5777
- ? rawThreadedFields.map((tf) => ({
5778
- valueField: tf,
5779
- accessorField: tf.varName === itemVar
5780
- ? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
5781
- : tf,
5782
- }))
5783
- : rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
5890
+ // See buildThreadedFieldPairs's doc for why the literal-value
5891
+ // search and the rendered accessor deliberately diverge for a
5892
+ // proven ancestor-scoped rebind.
5893
+ const threadedFieldPairs = buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes);
5784
5894
  // Accessor swap first: rewrites an already-templated `${payload.X}`
5785
5895
  // reference (from applyPayloadKeyValueSubstitutions) to this field's
5786
5896
  // real accessor. Each target (`${payload.X}`) is a unique, fully
@@ -5788,6 +5898,7 @@ pascalName = null) {
5788
5898
  // never re-shaped into `${payload.X}` form) can't re-match, so a
5789
5899
  // sequential pass here carries none of the reentrancy risk the
5790
5900
  // literal-value pass below has.
5901
+ let swapFiredForItemBoundField = false;
5791
5902
  const swapped = threadedFieldPairs.reduce((acc, { valueField, accessorField }) => {
5792
5903
  const replacement = `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`;
5793
5904
  // applyPayloadKeyValueSubstitutions only ever names a payload
@@ -5801,10 +5912,16 @@ pascalName = null) {
5801
5912
  // reference behind once the literal value itself has already
5802
5913
  // been replaced by the payload-key-value pass.
5803
5914
  const lastSegment = valueField.field.split(".").pop();
5915
+ const payloadFieldPlaceholder = `\${payload.${valueField.field}}`;
5916
+ const payloadLastSegmentPlaceholder = `\${payload.${lastSegment}}`;
5917
+ if (accessorField.varName === itemVar &&
5918
+ (acc.includes(payloadFieldPlaceholder) || acc.includes(payloadLastSegmentPlaceholder))) {
5919
+ swapFiredForItemBoundField = true;
5920
+ }
5804
5921
  return acc
5805
- .split(`\${payload.${valueField.field}}`)
5922
+ .split(payloadFieldPlaceholder)
5806
5923
  .join(replacement)
5807
- .split(`\${payload.${lastSegment}}`)
5924
+ .split(payloadLastSegmentPlaceholder)
5808
5925
  .join(replacement);
5809
5926
  }, text);
5810
5927
  // Literal-value substitution: ONE guarded regex-alternation pass over
@@ -5827,6 +5944,7 @@ pascalName = null) {
5827
5944
  : {
5828
5945
  value: stringValue,
5829
5946
  replacement: `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`,
5947
+ isItemBound: accessorField.varName === itemVar,
5830
5948
  };
5831
5949
  })
5832
5950
  .filter((b) => b !== null);
@@ -5841,6 +5959,25 @@ pascalName = null) {
5841
5959
  const filteredValueBindings = isProvenInvariant
5842
5960
  ? valueBindings.filter((b) => isGenuineVaryingQueryValue(b.value, chainCapture, allCaptures))
5843
5961
  : valueBindings;
5962
+ // Ground truth for the hoist gate, computed per FIELD from what
5963
+ // this call actually spliced into the request, rather than
5964
+ // inferred after the fact by re-scanning the fully rendered text
5965
+ // for `itemVar`: a target's OTHER join fields can independently
5966
+ // prove isAncestorScoped and rebind cleanly while THIS field's own
5967
+ // rebind attempt still falls back to `?? tf` (no structurally-
5968
+ // corresponding ancestor field exists for it). Deriving
5969
+ // `referencesItemVar` straight from the same value/placeholder
5970
+ // bindings that feed the splice below — rather than relying
5971
+ // solely on the post-hoc itemVar word-boundary scan — closes the
5972
+ // gap where a fallback accessor's occurrence in the final text
5973
+ // could otherwise be missed. A binding whose value never survives
5974
+ // to `filteredValueBindings` (unresolved, or excluded by the
5975
+ // zero-variance guard) is correctly excluded here too — it never
5976
+ // reaches the rendered request, so it can't leak an out-of-scope
5977
+ // identifier into it.
5978
+ if (swapFiredForItemBoundField || filteredValueBindings.some((b) => b.isItemBound)) {
5979
+ referencesItemVar = true;
5980
+ }
5844
5981
  const result = substituteThreadedValues(swapped, filteredValueBindings);
5845
5982
  const withDrillParamBindings = applyDrillParamBindings(foldReturnSpec, chainCapture, result);
5846
5983
  // The frozen-varying-param safety net exists to catch a
@@ -5872,8 +6009,26 @@ pascalName = null) {
5872
6009
  // there is no ancestor loop to hoist into in the first place (a
5873
6010
  // flat, single-level fold), so it is treated as item-scoped too —
5874
6011
  // existing flat folds must keep emitting byte-identical code.
5875
- const itemVarRefPattern = new RegExp(`\\$\\{${itemVar}[.[]`);
6012
+ // Word-boundary match on the bare identifier — not anchored to
6013
+ // `${itemVar` — because `scopedAccessor`/`unknownValueAccessor`
6014
+ // wraps every non-leaf hop in a `(itemVar.field as Record<string,
6015
+ // unknown>)` cast for a nested field path, so the identifier can
6016
+ // appear as `${(itemVar...` rather than immediately after `${`. An
6017
+ // anchored pattern misses that shape entirely and lets a genuinely
6018
+ // item-scoped reference get hoisted above the item loop, where
6019
+ // `itemVar` is never declared.
6020
+ const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
5876
6021
  let referencesItemVar = ancestorVars.length === 0;
6022
+ // A chain step that produces a response HEADER threads it through
6023
+ // `createHttpClient`'s single shared `bind` store (see
6024
+ // HttpClientOptions.bind), not a local variable — that store is
6025
+ // mutated in call order and read by whichever call happens to run
6026
+ // next, regardless of which item minted it. Running items
6027
+ // concurrently would let one item's header overwrite another's
6028
+ // in-flight header before its own later chain hop reads it back, so
6029
+ // any target relying on header threading keeps the item loop
6030
+ // sequential rather than risk cross-item header contamination.
6031
+ let chainUsesHeaderThreading = false;
5877
6032
  for (const chainIndex of target.chain) {
5878
6033
  const chainStep = actions[chainIndex];
5879
6034
  const chainRendered = rendered[chainIndex];
@@ -5894,8 +6049,10 @@ pascalName = null) {
5894
6049
  chainLines.push(` ${joined}`);
5895
6050
  chainLines.push(` schema: ${chainRendered.schemaExpr},`, ` })) as Record<string, unknown>;`);
5896
6051
  for (const p of chainStep.produces) {
5897
- if (p.kind === "header")
6052
+ if (p.kind === "header") {
6053
+ chainUsesHeaderThreading = true;
5898
6054
  continue;
6055
+ }
5899
6056
  if (chainDeclared.has(p.name))
5900
6057
  continue;
5901
6058
  if (!referencedNames.has(p.name))
@@ -5909,13 +6066,17 @@ pascalName = null) {
5909
6066
  const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
5910
6067
  if (referencesItemVar) {
5911
6068
  itemScopedLines.push(...chainLines, ...matchLines);
6069
+ if (chainLines.length > 0)
6070
+ hasItemScopedFetch = true;
6071
+ if (chainUsesHeaderThreading)
6072
+ hasItemScopedHeaderThreading = true;
5912
6073
  }
5913
6074
  else {
5914
6075
  hoistedChainLines.push(...chainLines);
5915
6076
  itemScopedLines.push(...matchLines);
5916
6077
  }
5917
6078
  }
5918
- lines.push(...hoistedChainLines, ...itemOpenLines, ...itemScopedLines, ...itemCloseLines, ...ancestorCloseLines, "");
6079
+ lines.push(...hoistedChainLines, ...emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch && !hasItemScopedHeaderThreading), ...ancestorCloseLines, "");
5919
6080
  continue;
5920
6081
  }
5921
6082
  // Every other chain step (already fully emitted, inline, by the fold
@@ -7027,29 +7188,44 @@ function findStructurallyCorrespondingAncestorField(ancestorScopes, field) {
7027
7188
  }
7028
7189
  return null;
7029
7190
  }
7030
- /** Recursively searches `obj` for the first array-valued property whose
7031
- * first element (itself a plain object) contains `lastSegment` as one of
7032
- * its own field paths' trailing segments, returning the full dot-path from
7033
- * `obj`'s root through the array's `0` index down to that field (e.g.
7034
- * `["entries", "0", "code"]`) — an accessor {@link pathToAccessor} renders
7035
- * as `.entries["0"].code`, valid whether the numeric segment is treated as
7036
- * a bracketed literal key or an array index. */
7191
+ /** Recursively searches `obj` for an array-valued property holding an
7192
+ * element (itself a plain object) that contains `lastSegment` as one of its
7193
+ * own field paths' trailing segments, returning the full dot-path from
7194
+ * `obj`'s root through the array's index down to that field (e.g.
7195
+ * `["entries", "1", "code"]`) — an accessor {@link pathToAccessor} renders
7196
+ * as `.entries["1"].code`, valid whether the numeric segment is treated as
7197
+ * a bracketed literal key or an array index. Scans every element of a
7198
+ * candidate array, not only the first: a sparse first element (e.g. a
7199
+ * sold-out entry missing an optional nested object that every OTHER sibling
7200
+ * genuinely carries) must not silently defeat a structural correspondence
7201
+ * that a later element in the same array would prove. This applies equally
7202
+ * to the recursive descent into a NESTED array-within-array: it also walks
7203
+ * every sibling element rather than only element `0`, so a sparse first
7204
+ * element one level up can't hide a match that lives in a nested array on a
7205
+ * later sibling. */
7037
7206
  function findFieldInFirstArrayElement(obj, lastSegment, path = []) {
7038
7207
  for (const [key, value] of Object.entries(obj)) {
7039
7208
  const childPath = [...path, key];
7040
7209
  if (Array.isArray(value)) {
7041
- const first = value[0];
7042
- if (first === null || typeof first !== "object" || Array.isArray(first))
7043
- continue;
7044
- const firstObj = first;
7045
- for (const { path: subPath } of walkItemFieldPaths(firstObj)) {
7046
- if (subPath[subPath.length - 1] === lastSegment) {
7047
- return [...childPath, "0", ...subPath];
7210
+ for (let i = 0; i < value.length; i++) {
7211
+ const candidate = value[i];
7212
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
7213
+ continue;
7214
+ const candidateObj = candidate;
7215
+ for (const { path: subPath } of walkItemFieldPaths(candidateObj)) {
7216
+ if (subPath[subPath.length - 1] === lastSegment) {
7217
+ return [...childPath, String(i), ...subPath];
7218
+ }
7048
7219
  }
7049
7220
  }
7050
- const nested = findFieldInFirstArrayElement(firstObj, lastSegment, [...childPath, "0"]);
7051
- if (nested)
7052
- return nested;
7221
+ for (let i = 0; i < value.length; i++) {
7222
+ const candidate = value[i];
7223
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
7224
+ continue;
7225
+ const nested = findFieldInFirstArrayElement(candidate, lastSegment, [...childPath, String(i)]);
7226
+ if (nested)
7227
+ return nested;
7228
+ }
7053
7229
  continue;
7054
7230
  }
7055
7231
  if (value !== null && typeof value === "object") {
@@ -7060,6 +7236,34 @@ function findFieldInFirstArrayElement(obj, lastSegment, path = []) {
7060
7236
  }
7061
7237
  return null;
7062
7238
  }
7239
+ /** Pairs each raw threaded field (whose VALUE is what a drill request's
7240
+ * captured text actually contains — the only thing a literal-value search
7241
+ * can ever find) with the accessor it should render as, shared by
7242
+ * {@link emitMultiStepExecuteHttp} and {@link emitContractTs}'s identical
7243
+ * fold-hoist rebind so the two emitters can never drift on this logic (see
7244
+ * this module's fold-plan investigation notes on the two call sites having
7245
+ * previously diverged). For a proven-ancestor-scoped target these
7246
+ * deliberately diverge: the literal search must still key off the item's own
7247
+ * field (the value textually present in the captured request), while the
7248
+ * emitted accessor points at the ancestor's structurally-corresponding field
7249
+ * instead — searching for the ANCESTOR field's (different) value would never
7250
+ * match anything in the rendered text and silently freeze the literal. When
7251
+ * no structurally-corresponding ancestor field exists, the accessor stays
7252
+ * item-bound (`?? tf`) — safe only because every caller's own itemVar
7253
+ * word-boundary scan over the FINAL rendered text (after this pairing feeds
7254
+ * substitution) is what actually decides whether the target may hoist above
7255
+ * its item loop, not this function. */
7256
+ function buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes) {
7257
+ if (!isAncestorScoped) {
7258
+ return rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
7259
+ }
7260
+ return rawThreadedFields.map((tf) => ({
7261
+ valueField: tf,
7262
+ accessorField: tf.varName === itemVar
7263
+ ? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
7264
+ : tf,
7265
+ }));
7266
+ }
7063
7267
  /** Every string, numeric, and boolean leaf value present anywhere in a response —
7064
7268
  * the set a chained drill-down step's request must overlap with for that
7065
7269
  * step to count as depending on this response. Deliberately walks the WHOLE
@@ -7551,6 +7755,41 @@ function parseFoldReturnSpec(flowFileContents) {
7551
7755
  return null;
7552
7756
  }
7553
7757
  }
7758
+ /**
7759
+ * Parses `browserFallbackGate`/`httpTimeoutMs` out of an object-form
7760
+ * recon-flow.json, mirroring {@link parseFoldReturnSpec}'s null-safe pattern:
7761
+ * a missing file, a legacy bare-array flow, or a malformed declaration all
7762
+ * resolve to an empty spec (today's behavior) rather than aborting
7763
+ * generation over a field that only tightens an opt-in guard.
7764
+ */
7765
+ function parseFallbackGateSpec(flowFileContents) {
7766
+ try {
7767
+ const raw = JSON.parse(flowFileContents);
7768
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
7769
+ return {};
7770
+ const { browserFallbackGate, httpTimeoutMs } = raw;
7771
+ const resolvedGate = (() => {
7772
+ if (browserFallbackGate === false)
7773
+ return false;
7774
+ if (Array.isArray(browserFallbackGate) &&
7775
+ browserFallbackGate.length > 0 &&
7776
+ browserFallbackGate.every((n) => typeof n === "string" && n.length > 0)) {
7777
+ return browserFallbackGate;
7778
+ }
7779
+ return undefined;
7780
+ })();
7781
+ const resolvedTimeout = typeof httpTimeoutMs === "number" && Number.isFinite(httpTimeoutMs) && httpTimeoutMs > 0
7782
+ ? httpTimeoutMs
7783
+ : undefined;
7784
+ return {
7785
+ ...(resolvedGate !== undefined ? { browserFallbackGate: resolvedGate } : {}),
7786
+ ...(resolvedTimeout !== undefined ? { httpTimeoutMs: resolvedTimeout } : {}),
7787
+ };
7788
+ }
7789
+ catch {
7790
+ return {};
7791
+ }
7792
+ }
7554
7793
  /** The JS `typeof` a `drillParamBindings` entry's `default` must match for
7555
7794
  * its declared `type` — the same "assert the type, then check the default
7556
7795
  * agrees" pattern the rest of {@link parseFoldReturnSpec} uses for its other
@@ -8659,11 +8898,17 @@ function buildNestedSpreadOverride(base, path, leafExpr) {
8659
8898
  return `{ ...${base}, ${keyExpr}: ${nested} }`;
8660
8899
  }
8661
8900
  /**
8662
- * Emits a bounded paging loop for a read-only GraphQL primary operation
8663
- * exposing {@link PaginationSignal}: advances the skip/offset variable by
8664
- * the observed page size on each call, stops once the response's own
8665
- * reported total is reached or `MAX_PAGES` caps it, and merges pages by the
8666
- * detected identity field rather than concatenating blindly.
8901
+ * Emits a bounded paging loop for a read-only primary operation exposing
8902
+ * {@link PaginationSignal}: advances the skip/offset variable by the
8903
+ * observed page size on each call, stops once the response's own reported
8904
+ * total is reached or `MAX_PAGES` caps it, and merges pages by the detected
8905
+ * identity field rather than concatenating blindly.
8906
+ *
8907
+ * The primary operation may be GraphQL (each page issued via `getGql`) or a
8908
+ * plain REST endpoint (each page issued via `httpClient`) — `fetchCall`
8909
+ * selects which, and is the only call-shape-specific piece of the loop; the
8910
+ * skip/PAGE_SIZE/MAX_PAGES bookkeeping, de-dup, and halt guards below are
8911
+ * shared verbatim between both.
8667
8912
  *
8668
8913
  * `foldMergeLines`, when non-empty, threads a resolved single-primary fold
8669
8914
  * plan (see `emitContractTs`'s `singlePrimaryFoldPlans`) additively into the
@@ -8673,9 +8918,14 @@ function buildNestedSpreadOverride(base, path, leafExpr) {
8673
8918
  * mutates its item in place (`Object.assign`), which is visible through
8674
8919
  * `itemsById`'s own stored references — no re-`set` needed.
8675
8920
  */
8676
- function buildPaginatedGqlExecuteHttpBody(opts) {
8677
- const { pascal, gqlOperationNameExpr, queryConstName, gqlVariablesExpr, signal, foldMergeLines } = opts;
8921
+ function buildPaginatedFetchLoopExecuteHttpBody(opts) {
8922
+ const { pascal, gqlVariablesExpr, signal, foldMergeLines, fetchCall, valueConstraints } = opts;
8678
8923
  const { totalPath, arrayPath, containerPath, countKey, skipKey, pageSize, identityField } = signal;
8924
+ const declaredMax = valueConstraints?.[countKey]?.max;
8925
+ const pageSizeDefault = declaredMax ?? pageSize;
8926
+ const pageFetchExpr = (variablesExpr) => fetchCall.kind === "gql"
8927
+ ? `getGql(context.baseUrl)(${fetchCall.gqlOperationNameExpr}, ${fetchCall.queryConstName}, ${variablesExpr})`
8928
+ : `httpClient(\`\${context.baseUrl}${fetchCall.endpointPath}\`, { method: "POST", body: JSON.stringify(${variablesExpr}) })`;
8679
8929
  const countKeyExpr = isValidJsIdentifier(countKey) ? countKey : JSON.stringify(countKey);
8680
8930
  const skipKeyExpr = isValidJsIdentifier(skipKey) ? skipKey : JSON.stringify(skipKey);
8681
8931
  const paginationBase = pathAccessExpr("baseVariables", containerPath);
@@ -8688,32 +8938,55 @@ function buildPaginatedGqlExecuteHttpBody(opts) {
8688
8938
  // `lastPage` is typed as ${pascal}Response (never null): the first page is
8689
8939
  // fetched before the loop starts, so there's nothing left to narrow.
8690
8940
  const withItemsOverrideExpr = buildNestedSpreadOverride("lastPage", arrayPath, "[...itemsById.values()]");
8691
- // When MAX_PAGES caps the loop before `total` converges, the merged
8692
- // envelope's own total must say so instead of repeating the API's
8693
- // original (larger) reported total — otherwise a truncated payload still
8694
- // claims to be complete.
8695
- const withTotalOverrideExpr = buildNestedSpreadOverride("withItems", totalPath, `truncated ? itemsById.size : ${pathAccessExpr("withItems", totalPath)}`);
8941
+ // The server's own reported total is left untouched — overwriting it with
8942
+ // the delivered count would make a MAX_PAGES-capped fetch indistinguishable
8943
+ // from a genuinely complete one. `deliveredCount`/`truncated` are added as
8944
+ // siblings on the response so a caller can tell "total=5000, delivered=100,
8945
+ // truncated=true" apart from "total=436, delivered=436, truncated=false".
8946
+ const withTotalOverrideExpr = `{ ...withItems, deliveredCount: itemsById.size, truncated }`;
8696
8947
  return ` const baseVariables = ${gqlVariablesExpr};
8697
- const PAGE_SIZE = ${pageSize};
8948
+ const PAGE_SIZE = payload.pageSize ?? ${pageSizeDefault};
8698
8949
  // Bounded so a paging bug (a total that never converges) can't loop forever.
8699
8950
  const MAX_PAGES = payload.maxPages ?? 50;
8700
8951
  const itemsById = new Map<string, ${itemTypeExpr}>();
8701
8952
  let skip = 0;
8702
- const page = await getGql(context.baseUrl)(${gqlOperationNameExpr}, ${queryConstName}, ${variablesForCall});
8953
+ const page = await ${pageFetchExpr(variablesForCall)};
8703
8954
  let lastPage: ${pascal}Response = page;
8704
8955
  let total = ${totalAccessExpr};
8705
- for (const item of ${arrayAccessExpr}) {
8956
+ const firstPageItems = ${arrayAccessExpr};
8957
+ for (const item of firstPageItems) {
8706
8958
  itemsById.set(String(${identityAccessExpr}), item);
8707
8959
  }
8708
8960
  skip += PAGE_SIZE;
8709
- for (let pageIndex = 1; pageIndex < MAX_PAGES && itemsById.size < total; pageIndex++) {
8710
- const page = await getGql(context.baseUrl)(${gqlOperationNameExpr}, ${queryConstName}, ${variablesForCall});
8961
+ // A first page shorter than what was asked for is the same "nothing left"
8962
+ // signal as a short subsequent page — stop before ever entering the loop
8963
+ // instead of issuing a request the server already told us would come
8964
+ // back empty.
8965
+ const firstPageWasShort = firstPageItems.length < PAGE_SIZE;
8966
+ for (
8967
+ let pageIndex = 1;
8968
+ !firstPageWasShort && pageIndex < MAX_PAGES && itemsById.size < total;
8969
+ pageIndex++
8970
+ ) {
8971
+ const page = await ${pageFetchExpr(variablesForCall)};
8711
8972
  lastPage = page;
8712
8973
  total = ${totalAccessExpr};
8713
- for (const item of ${arrayAccessExpr}) {
8974
+ const sizeBeforePage = itemsById.size;
8975
+ const pageItems = ${arrayAccessExpr};
8976
+ for (const item of pageItems) {
8714
8977
  itemsById.set(String(${identityAccessExpr}), item);
8715
8978
  }
8979
+ // A server-reported total that doesn't exactly match the count of
8980
+ // distinct items actually returned would otherwise drive the loop to
8981
+ // MAX_PAGES worth of wasted empty requests; stop as soon as a page
8982
+ // contributes nothing new.
8983
+ if (itemsById.size === sizeBeforePage) break;
8716
8984
  skip += PAGE_SIZE;
8985
+ // A page shorter than what was asked for is the server's own signal
8986
+ // that nothing is left, whether or not its reported total agrees —
8987
+ // stop here instead of issuing a request the server already told us
8988
+ // would come back empty.
8989
+ if (pageItems.length < PAGE_SIZE) break;
8717
8990
  }
8718
8991
  ${foldMergeLines.length > 0 ? `${foldMergeLines.join("\n")}\n` : ""} const truncated = itemsById.size < total;
8719
8992
  const withItems = ${withItemsOverrideExpr};
@@ -8761,7 +9034,21 @@ function buildContractChecklist(opts) {
8761
9034
  /** Generates a complete contract.ts source string for a plugin — exported so
8762
9035
  * unit tests can drive the emitter directly without spawning the CLI. */
8763
9036
  function emitContractTs(opts) {
8764
- const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, payloadFieldNames, optionalPayloadFieldNames = new Set(), headerBindings = [], unpopulatedDeclaredVariables = [], } = opts;
9037
+ const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, valueConstraints = value_constraints_1.EMPTY_VALUE_CONSTRAINTS, payloadFieldNames, optionalPayloadFieldNames = new Set(), headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
9038
+ const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
9039
+ /** Rendered `meta.browserFallbackGate` literal: `false` verbatim, a
9040
+ * list-derived arrow-function predicate matching against `error.name`, or
9041
+ * "" (omitted) when the flow declared no gate — preserving byte-identical
9042
+ * output for every flow that doesn't opt in. */
9043
+ const browserFallbackGateLiteral = browserFallbackGate === false
9044
+ ? "\n browserFallbackGate: false,"
9045
+ : browserFallbackGate !== undefined
9046
+ ? `\n browserFallbackGate: (error) => ${JSON.stringify(browserFallbackGate)}.includes(error.name),`
9047
+ : "";
9048
+ /** Rendered `createHttpClient({ ..., defaultTimeoutMs })` fragment — "" when
9049
+ * the flow declared no `httpTimeoutMs`, so the call is byte-identical to
9050
+ * today's when the key is absent. */
9051
+ const defaultTimeoutMsOption = httpTimeoutMs !== undefined ? `, defaultTimeoutMs: ${httpTimeoutMs}` : "";
8765
9052
  // This is the CLIENT-level schema — createHttpClient's default, and the
8766
9053
  // plugin's caller-facing contract (what executeHttp's return value promises
8767
9054
  // its own caller). It does NOT validate any individual call in a multi-step
@@ -8850,11 +9137,20 @@ function emitContractTs(opts) {
8850
9137
  : inputBody
8851
9138
  ? `z.object({})`
8852
9139
  : `z.object({\n query: z.string().min(1),\n})`;
8853
- // Only the single-endpoint GraphQL read path (a real primary operation, no
9140
+ // Only the single-endpoint read path (a real primary operation, no
8854
9141
  // multi-step flow) is a candidate for a paging signal — multiStepBody
8855
- // already owns its own per-call semantics.
8856
- const paginationSignal = !multiStepBody && gql && gqlOperationName
8857
- ? detectPaginationSignal(responseBody, gqlVariables, `${endpointPath}::${gqlOperationName}`, allCaptures)
9142
+ // already owns its own per-call semantics. A REST primary has no
9143
+ // operationName/query, so it already collapses to the same identity
9144
+ // operationGroupKey falls back to for such a capture
9145
+ // (`${endpointPath}::anonymous`) — this is the same identity
9146
+ // detectPaginationSignal uses to match sibling captures below.
9147
+ const paginationOperationIdentity = gql
9148
+ ? gqlOperationName
9149
+ ? `${endpointPath}::${gqlOperationName}`
9150
+ : null
9151
+ : `${endpointPath}::anonymous`;
9152
+ const paginationSignal = !multiStepBody && paginationOperationIdentity
9153
+ ? detectPaginationSignal(responseBody, gqlVariables, paginationOperationIdentity, allCaptures)
8858
9154
  : null;
8859
9155
  // A resolved drill-down fold plan on the single-primary getGql/httpClient
8860
9156
  // hot path (`multiStepBody` unset — see emitMultiStepExecuteHttp for the
@@ -8864,7 +9160,7 @@ function emitContractTs(opts) {
8864
9160
  // drop the fold feature the flow author declared (see
8865
9161
  // recon-generate-foldreturn-regresses-primary-op-and-payload-to-ats-submission-shape.md).
8866
9162
  // Also threaded additively into `paginationSignal`'s fetch loop below (see
8867
- // buildPaginatedGqlExecuteHttpBody) — the fold runs against the final
9163
+ // buildPaginatedFetchLoopExecuteHttpBody) — the fold runs against the final
8868
9164
  // assembled/de-duplicated page items, not just the first page's captured
8869
9165
  // sample, so a paginated primary is no longer excluded from folding.
8870
9166
  const singlePrimaryFoldPlans = resolveApplicableFoldPlans(actionSteps, foldReturnSpec, multiStepBody);
@@ -8905,12 +9201,13 @@ function emitContractTs(opts) {
8905
9201
  const addExtendField = (name, line) => {
8906
9202
  extendFields.set(name, line);
8907
9203
  };
8908
- // A detected bounded-paging signal means buildPaginatedGqlExecuteHttpBody
9204
+ // A detected bounded-paging signal means buildPaginatedFetchLoopExecuteHttpBody
8909
9205
  // will emit a loop bounded by MAX_PAGES — expose that bound as a caller-
8910
9206
  // overridable payload field, mirroring how PAGE_SIZE is already sourced
8911
9207
  // from the detected signal.
8912
9208
  if (paginationSignal) {
8913
9209
  addExtendField("maxPages", " maxPages: z.number().int().positive().optional(),");
9210
+ addExtendField("pageSize", " pageSize: z.number().int().positive().optional(),");
8914
9211
  }
8915
9212
  // The base extend's own keys — job-application submission flows only.
8916
9213
  if (usesApplicantContactSchema) {
@@ -9019,18 +9316,27 @@ function emitContractTs(opts) {
9019
9316
  ? [...discoveredAdditionalBodyKeys.entries()].sort(([a], [b]) => a.localeCompare(b))
9020
9317
  : [];
9021
9318
  let usesMultipartBoolean = false;
9022
- for (const [name, kind] of sortedAdditionalKeys) {
9319
+ for (const [name, info] of sortedAdditionalKeys) {
9023
9320
  if (isReservedByApplicantContactSchema(name))
9024
9321
  continue;
9025
9322
  // Use multipartBoolean() for booleans when multipart is in play, so
9026
9323
  // multipart string-encoded "true"/"false" round-trip to native booleans
9027
9324
  // (matches the inputBody boolean handling for parity).
9028
- const zod = kind === "string"
9029
- ? "z.string()"
9030
- : kind === "number"
9031
- ? payloadNeedsMultipart
9032
- ? "z.coerce.number()"
9033
- : "z.number()"
9325
+ // A closed-set vocabulary (string) or a discovered capacity ceiling
9326
+ // (number) narrows the caller-facing schema instead of falling through
9327
+ // to the unconstrained default — see {@link AdditionalBodyKeyInfo}.
9328
+ const zod = info.kind === "string"
9329
+ ? info.enumValues
9330
+ ? `z.enum([${info.enumValues.map((v) => JSON.stringify(v)).join(", ")}])`
9331
+ : "z.string()"
9332
+ : info.kind === "number"
9333
+ ? info.capacityMax !== undefined
9334
+ ? payloadNeedsMultipart
9335
+ ? `z.coerce.number().max(${info.capacityMax})`
9336
+ : `z.number().max(${info.capacityMax})`
9337
+ : payloadNeedsMultipart
9338
+ ? "z.coerce.number()"
9339
+ : "z.number()"
9034
9340
  : payloadNeedsMultipart
9035
9341
  ? "multipartBoolean()"
9036
9342
  : "z.boolean()";
@@ -9117,6 +9423,32 @@ function emitContractTs(opts) {
9117
9423
  extendFields.set(fieldName, line.replace(/,\s*$/, ".optional(),"));
9118
9424
  }
9119
9425
  }
9426
+ // A consumer-declared value constraint overrides its matching field's
9427
+ // emitted Zod expression here, at the single merge point every discovery
9428
+ // source above funnels through — see {@link ReconValueConstraints}. A
9429
+ // declared field name with no entry in extendFields never appeared in any
9430
+ // capture this run and is a no-op, not an error: there is no schema line
9431
+ // for it to attach to.
9432
+ for (const [fieldName, constraint] of Object.entries(valueConstraints)) {
9433
+ const line = extendFields.get(fieldName);
9434
+ if (line === null || line === undefined)
9435
+ continue;
9436
+ if (constraint.enumValues === undefined &&
9437
+ constraint.min === undefined &&
9438
+ constraint.max === undefined) {
9439
+ continue;
9440
+ }
9441
+ const key = isValidJsIdentifier(fieldName) ? fieldName : JSON.stringify(fieldName);
9442
+ const zod = constraint.enumValues
9443
+ ? `z.enum([${constraint.enumValues.map((v) => JSON.stringify(v)).join(", ")}])`
9444
+ : [
9445
+ "z.number()",
9446
+ ...(constraint.min !== undefined ? [`.min(${constraint.min})`] : []),
9447
+ ...(constraint.max !== undefined ? [`.max(${constraint.max})`] : []),
9448
+ ].join("");
9449
+ const optional = line.trimEnd().endsWith(".optional(),");
9450
+ extendFields.set(fieldName, ` ${key}: ${zod}${optional ? ".optional()" : ""},`);
9451
+ }
9120
9452
  const mergedExtension = extendFields.size > 0 ? `.extend({\n${[...extendFields.values()].join("\n")}\n})` : "";
9121
9453
  const payloadSchemaExpr = `${basePayloadSchemaExpr}${mergedExtension}`;
9122
9454
  // basePayloadSchemaExpr's own Answers field always wraps in
@@ -9196,18 +9528,24 @@ ${
9196
9528
  // GraphQL to the primary endpoint.
9197
9529
  needsFoldHttpClient
9198
9530
  ? `
9199
- const httpClient = createHttpClient({ schema: z.unknown(), bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
9531
+ const httpClient = createHttpClient({ schema: z.unknown(), bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)}${defaultTimeoutMsOption} });
9200
9532
  `
9201
9533
  : ""}`
9202
9534
  : `
9203
- const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
9535
+ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)}${defaultTimeoutMsOption} });
9204
9536
  `;
9205
9537
  const gqlOperationNameExpr = gqlOperationName
9206
9538
  ? JSON.stringify(gqlOperationName)
9207
9539
  : JSON.stringify(`${pascal}Search`);
9540
+ // A REST primary with a detected paging signal renders `baseVariables` from
9541
+ // the same captured request-variables object the signal was itself
9542
+ // detected from (see paginationOperationIdentity above) — the default
9543
+ // `{ q: payload.query }` REST body has no skip/count container to advance.
9208
9544
  const gqlVariablesExpr = gqlOperationName
9209
9545
  ? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
9210
- : "{ q: payload.query }";
9546
+ : paginationSignal
9547
+ ? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
9548
+ : "{ q: payload.query }";
9211
9549
  /** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
9212
9550
  * — that fold every resolved plan's drill-down data onto `dataVarName`'s
9213
9551
  * primary array — the single-primary counterpart of
@@ -9273,7 +9611,19 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9273
9611
  // Every target's join/merge — plus any non-hoistable target's own
9274
9612
  // chain fetch — goes here, spliced inside the item loop.
9275
9613
  const itemScopedLines = [];
9276
- const itemVarRefPattern = new RegExp(`\\$\\{${itemVar}[.[]`);
9614
+ // True once any target's own chain fetch ends up item-scoped — see
9615
+ // emitMultiStepExecuteHttp's identical flag and emitItemLoopLines.
9616
+ let hasItemScopedFetch = false;
9617
+ // True once any item-scoped target's chain threads a response header
9618
+ // through createHttpClient's shared `bind` store — see
9619
+ // emitMultiStepExecuteHttp's identical flag for why that disqualifies
9620
+ // parallelizing the item loop.
9621
+ let hasItemScopedHeaderThreading = false;
9622
+ // Word-boundary match — see emitMultiStepExecuteHttp's identical
9623
+ // `itemVarRefPattern` for why an anchored `${itemVar` pattern misses a
9624
+ // nested field path's `${(itemVar.field as Record<string,
9625
+ // unknown>)...}` cast wrapper.
9626
+ const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
9277
9627
  for (const [targetIndex, target] of foldPlan.targets.entries()) {
9278
9628
  const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
9279
9629
  if (!matchedPrimaryItem) {
@@ -9343,29 +9693,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9343
9693
  ...target.joinFields.map((field) => ({ varName: itemVar, field })),
9344
9694
  ...findThreadedJoinFields(threadingScopes, chainCapture, allCaptures),
9345
9695
  ]);
9346
- // Mirrors emitMultiStepExecuteHttp's identical rebind (see
9347
- // isAncestorScoped above): a proven ancestor-scoped drill still
9348
- // rebinds fields findThreadedJoinFields left on itemVar purely
9349
- // because the matched item's own field was the only object in
9350
- // scope whose value equalled the captured literal.
9351
- // Pairs each raw threaded field (whose VALUE is what's actually
9352
- // present in `rawUrl` — the only thing a literal-value search can
9353
- // ever find) with the accessor it should render as. For an
9354
- // ancestor-scoped rebind these deliberately diverge: the literal
9355
- // search must still key off the item's own field (that's the
9356
- // value textually present in the URL), while the emitted accessor
9357
- // points at the ancestor's structurally-corresponding field
9358
- // instead — searching for the ANCESTOR field's (different) value
9359
- // would never match anything in the URL and silently freeze the
9360
- // literal.
9361
- const threadedFieldPairs = isAncestorScoped
9362
- ? rawThreadedFields.map((tf) => ({
9363
- valueField: tf,
9364
- accessorField: tf.varName === itemVar
9365
- ? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
9366
- : tf,
9367
- }))
9368
- : rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
9696
+ // Mirrors emitMultiStepExecuteHttp's identical rebind — see
9697
+ // buildThreadedFieldPairs's doc for why the literal-value search
9698
+ // and the rendered accessor deliberately diverge for a proven
9699
+ // ancestor-scoped rebind.
9700
+ const threadedFieldPairs = buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes);
9369
9701
  // ONE guarded regex-alternation pass over `withBase` for every
9370
9702
  // threaded field's value, longest first — see substituteThreadedValues's
9371
9703
  // doc for why a per-field sequential `.replace()` loop here (the bug
@@ -9387,6 +9719,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9387
9719
  : {
9388
9720
  value: stringValue,
9389
9721
  replacement: `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`,
9722
+ isItemBound: accessorField.varName === itemVar,
9390
9723
  };
9391
9724
  })
9392
9725
  .filter((b) => b !== null);
@@ -9399,6 +9732,20 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9399
9732
  const filteredValueBindings = isProvenInvariant
9400
9733
  ? valueBindings.filter((b) => isGenuineVaryingQueryValue(b.value, chainCapture, allCaptures))
9401
9734
  : valueBindings;
9735
+ // Mirrors emitMultiStepExecuteHttp's identical ground-truth hoist
9736
+ // signal: derive `referencesItemVar` straight from the same
9737
+ // value bindings that feed the splice below, rather than relying
9738
+ // solely on the post-hoc itemVar word-boundary scan — a target's
9739
+ // OTHER join fields can independently prove isAncestorScoped and
9740
+ // rebind cleanly while THIS field's own rebind attempt still
9741
+ // falls back to `?? tf` (no structurally-corresponding ancestor
9742
+ // field exists for it). A binding excluded from
9743
+ // `filteredValueBindings` (unresolved, or dropped by the
9744
+ // zero-variance guard) never reaches the rendered URL, so it's
9745
+ // correctly excluded here too.
9746
+ if (filteredValueBindings.some((b) => b.isItemBound)) {
9747
+ referencesItemVar = true;
9748
+ }
9402
9749
  const result = substituteThreadedValues(withBase, filteredValueBindings);
9403
9750
  const withDrillParamBindings = applyDrillParamBindings(foldReturnSpec, chainCapture, result);
9404
9751
  // See emitMultiStepExecuteHttp's identical guard: the frozen-
@@ -9419,10 +9766,20 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9419
9766
  // treated as item-scoped too — flat folds must keep emitting
9420
9767
  // byte-identical code.
9421
9768
  let referencesItemVar = ancestorVars.length === 0;
9769
+ // See emitMultiStepExecuteHttp's identical `chainUsesHeaderThreading`
9770
+ // for why any header-kind produce disqualifies parallelizing the
9771
+ // item loop — httpClient's shared `bind` store applies to every call
9772
+ // this function's own chainLines make too, even though (unlike
9773
+ // emitMultiStepExecuteHttp) this codegen path never renders a local
9774
+ // variable for the produced header itself.
9775
+ let chainUsesHeaderThreading = false;
9422
9776
  for (const chainIndex of target.chain) {
9423
9777
  const chainStep = actionSteps[chainIndex];
9424
9778
  if (!chainStep)
9425
9779
  continue;
9780
+ if (chainStep.produces.some((p) => p.kind === "header")) {
9781
+ chainUsesHeaderThreading = true;
9782
+ }
9426
9783
  // The zero-variance guard lives inside `parameterizeUrl` itself
9427
9784
  // (see above) so it can skip only the threaded-value splice while
9428
9785
  // still letting a spec-declared drillParamBindings substitution
@@ -9444,13 +9801,17 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9444
9801
  const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
9445
9802
  if (referencesItemVar) {
9446
9803
  itemScopedLines.push(...chainLines, ...matchLines);
9804
+ if (chainLines.length > 0)
9805
+ hasItemScopedFetch = true;
9806
+ if (chainUsesHeaderThreading)
9807
+ hasItemScopedHeaderThreading = true;
9447
9808
  }
9448
9809
  else {
9449
9810
  hoistedChainLines.push(...chainLines);
9450
9811
  itemScopedLines.push(...matchLines);
9451
9812
  }
9452
9813
  }
9453
- lines.push(...hoistedChainLines, ...itemOpenLines, ...itemScopedLines, ...itemCloseLines, ...ancestorCloseLines, "");
9814
+ lines.push(...hoistedChainLines, ...emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch && !hasItemScopedHeaderThreading), ...ancestorCloseLines, "");
9454
9815
  }
9455
9816
  return lines;
9456
9817
  };
@@ -9469,13 +9830,19 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
9469
9830
  const executeHttpBody = multiStepBody
9470
9831
  ? multiStepBody
9471
9832
  : paginationSignal
9472
- ? buildPaginatedGqlExecuteHttpBody({
9833
+ ? buildPaginatedFetchLoopExecuteHttpBody({
9473
9834
  pascal,
9474
- gqlOperationNameExpr,
9475
- queryConstName: `${pascal.toUpperCase()}_QUERY`,
9476
9835
  gqlVariablesExpr,
9477
9836
  signal: paginationSignal,
9478
9837
  foldMergeLines: paginatedFoldMergeLines,
9838
+ valueConstraints,
9839
+ fetchCall: gql
9840
+ ? {
9841
+ kind: "gql",
9842
+ gqlOperationNameExpr,
9843
+ queryConstName: `${pascal.toUpperCase()}_QUERY`,
9844
+ }
9845
+ : { kind: "rest", endpointPath },
9479
9846
  })
9480
9847
  : gql
9481
9848
  ? ` const data = await getGql(context.baseUrl)(${gqlOperationNameExpr}, ${pascal.toUpperCase()}_QUERY, ${gqlVariablesExpr});
@@ -9592,7 +9959,7 @@ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
9592
9959
  // since the multipart wire format is what makes that field's
9593
9960
  // JSON-stringified encoding parseable.
9594
9961
  `
9595
- : ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}
9962
+ : ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}${browserFallbackGateLiteral}
9596
9963
  },
9597
9964
  ${executeHttpMethodBlock}
9598
9965
  /** Browser fallback: Stagehand + Steel — invoked only when hot path fails. */
@@ -10132,6 +10499,21 @@ async function resolveFormSchema(specifier) {
10132
10499
  logger.info(`form-schema: ${specifier === load_form_schema_1.FORM_SCHEMA_NONE ? "none (no ATS form recovery)" : `custom keys from ${specifier}`}`);
10133
10500
  return formSchema;
10134
10501
  }
10502
+ /**
10503
+ * Resolves the consumer-declared value constraints for this run.
10504
+ *
10505
+ * Absent `--value-constraints`, no override happens: every field's emitted
10506
+ * Zod expression stays whatever its discovery mechanism (Phase E/F options,
10507
+ * form-schema fields, structured keys) already inferred — the same "absence
10508
+ * means none" discipline `--vocabulary`/`--form-schema` use.
10509
+ */
10510
+ async function resolveValueConstraints(specifier) {
10511
+ if (!specifier)
10512
+ return value_constraints_1.EMPTY_VALUE_CONSTRAINTS;
10513
+ const valueConstraints = await (0, load_value_constraints_1.loadReconValueConstraints)(specifier, process.cwd());
10514
+ logger.info(`value-constraints: ${specifier === load_value_constraints_1.VALUE_CONSTRAINTS_NONE ? "none (no field overrides)" : `custom constraints from ${specifier}`}`);
10515
+ return valueConstraints;
10516
+ }
10135
10517
  /**
10136
10518
  * The capture whose own response `assertRequiredUrlFieldsReferenced`'s
10137
10519
  * scan actually describes: a submission flow returns
@@ -10296,6 +10678,7 @@ async function main() {
10296
10678
  let emit = "ts";
10297
10679
  let vocabularySpecifier = "";
10298
10680
  let formSchemaSpecifier = "";
10681
+ let valueConstraintsSpecifier = "";
10299
10682
  let runDir;
10300
10683
  let allowEmptyCapture = false;
10301
10684
  for (let i = 0; i < args.length; i++) {
@@ -10305,6 +10688,8 @@ async function main() {
10305
10688
  vocabularySpecifier = args[++i];
10306
10689
  else if (args[i] === "--form-schema" && args[i + 1])
10307
10690
  formSchemaSpecifier = args[++i];
10691
+ else if (args[i] === "--value-constraints" && args[i + 1])
10692
+ valueConstraintsSpecifier = args[++i];
10308
10693
  else if (args[i] === "--run-dir" && args[i + 1])
10309
10694
  runDir = args[++i];
10310
10695
  else if (args[i] === "--force")
@@ -10374,7 +10759,7 @@ async function main() {
10374
10759
  return [];
10375
10760
  }
10376
10761
  })();
10377
- const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, } = (() => {
10762
+ const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, fallbackGateSpec, } = (() => {
10378
10763
  const flowFileContents = (() => {
10379
10764
  try {
10380
10765
  return (0, node_fs_1.readFileSync)(flowFile, "utf8");
@@ -10387,6 +10772,10 @@ async function main() {
10387
10772
  // valid `foldReturn` still resolves when the rest of the flow file is
10388
10773
  // degenerate — the two declarations fail independently.
10389
10774
  const foldReturnSpec = flowFileContents === null ? null : parseFoldReturnSpec(flowFileContents);
10775
+ // Same independence rule as `foldReturnSpec` above: an empty spec
10776
+ // (missing/malformed declaration) is indistinguishable from "not
10777
+ // declared" and preserves today's byte-identical output.
10778
+ const fallbackGateSpec = flowFileContents === null ? {} : parseFallbackGateSpec(flowFileContents);
10390
10779
  try {
10391
10780
  const raw = flowFileContents === null ? null : JSON.parse(flowFileContents);
10392
10781
  if (Array.isArray(raw))
@@ -10398,6 +10787,7 @@ async function main() {
10398
10787
  requireSubmitEndpointMatch: false,
10399
10788
  displayName: undefined,
10400
10789
  foldReturnSpec,
10790
+ fallbackGateSpec,
10401
10791
  };
10402
10792
  if (raw !== null &&
10403
10793
  typeof raw === "object" &&
@@ -10412,6 +10802,7 @@ async function main() {
10412
10802
  requireSubmitEndpointMatch: obj.requireSubmitEndpointMatch ?? false,
10413
10803
  displayName: obj.displayName,
10414
10804
  foldReturnSpec,
10805
+ fallbackGateSpec,
10415
10806
  };
10416
10807
  }
10417
10808
  return {
@@ -10422,6 +10813,7 @@ async function main() {
10422
10813
  requireSubmitEndpointMatch: false,
10423
10814
  displayName: undefined,
10424
10815
  foldReturnSpec,
10816
+ fallbackGateSpec,
10425
10817
  };
10426
10818
  }
10427
10819
  catch {
@@ -10433,6 +10825,7 @@ async function main() {
10433
10825
  requireSubmitEndpointMatch: false,
10434
10826
  displayName: undefined,
10435
10827
  foldReturnSpec,
10828
+ fallbackGateSpec,
10436
10829
  };
10437
10830
  }
10438
10831
  })();
@@ -10450,6 +10843,10 @@ async function main() {
10450
10843
  // Consumer-supplied wire keys for ATS form-schema recovery, or null. When
10451
10844
  // null the recovery functions no-op — the engine hardcodes no vendor format.
10452
10845
  const formSchema = await resolveFormSchema(formSchemaSpecifier);
10846
+ // Consumer-declared domain facts (enum/min/max) that no captured response
10847
+ // leaf exposes a shape for, or EMPTY_VALUE_CONSTRAINTS when absent — see
10848
+ // {@link ReconValueConstraints}.
10849
+ const valueConstraints = await resolveValueConstraints(valueConstraintsSpecifier);
10453
10850
  const pascal = toPascalCase(siteId);
10454
10851
  // Read the flow's declared own-backend hosts BEFORE deriving baseUrl, so
10455
10852
  // deriveBaseUrl itself can gate on them: a third-party host that happens
@@ -10871,7 +11268,12 @@ async function main() {
10871
11268
  parsedOperationName(primaryGraphQLOperation.capture.query ?? ""))
10872
11269
  : (fallbackGraphQLCapture?.operationName ??
10873
11270
  parsedOperationName(fallbackGraphQLCapture?.query ?? "")),
10874
- gqlVariables: primaryGraphQLOperation?.capture.variables ?? null,
11271
+ // For a REST primary (no primaryGraphQLOperation), the winning
11272
+ // capture's own `variables`/`decodedParams` is the REST equivalent of
11273
+ // a GraphQL operation's captured variables — the object
11274
+ // detectPaginationSignal walks to find a skip/count container.
11275
+ gqlVariables: primaryGraphQLOperation?.capture.variables ??
11276
+ (!gql ? (winningCapture?.variables ?? winningCapture?.decodedParams ?? null) : null),
10875
11277
  allCaptures: activeCaptures,
10876
11278
  auxFiles,
10877
11279
  multiStepBody,
@@ -10887,10 +11289,12 @@ async function main() {
10887
11289
  discoveredRawOptionFields,
10888
11290
  discoveredAdditionalBodyKeys,
10889
11291
  discoveredStructuredKeys,
11292
+ valueConstraints,
10890
11293
  payloadFieldNames: browserFlow.payloadFieldNames,
10891
11294
  optionalPayloadFieldNames: browserFlow.optionalPayloadFieldNames,
10892
11295
  headerBindings,
10893
11296
  unpopulatedDeclaredVariables: primaryGraphQLOperation?.unpopulatedDeclaredVariables ?? [],
11297
+ fallbackGateSpec,
10894
11298
  };
10895
11299
  const contractCode = emitContractTs(contractOpts);
10896
11300
  return {