@enricai/barnacle 1.12.76 → 1.12.77

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.
@@ -66,6 +66,7 @@ exports.collectUnboundLiterals = collectUnboundLiterals;
66
66
  exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
67
67
  exports.countSpliceableFacets = countSpliceableFacets;
68
68
  exports.spliceFacetsIntoStringVariable = spliceFacetsIntoStringVariable;
69
+ exports.spliceFacetsIntoArrayVariable = spliceFacetsIntoArrayVariable;
69
70
  exports.findAllObjectArrayFields = findAllObjectArrayFields;
70
71
  exports.findAllObjectArrayFieldsOrWholeObject = findAllObjectArrayFieldsOrWholeObject;
71
72
  exports.applyDrillParamBindings = applyDrillParamBindings;
@@ -508,7 +509,7 @@ function extractNavigateToFacetOrder(flowSteps) {
508
509
  previousNavigateToHash = currentHash;
509
510
  if (value === null || value.length === 0)
510
511
  continue;
511
- order.push({ value, field: step.payloadField });
512
+ order.push({ value, field: step.payloadField, optional: step.optional === true });
512
513
  }
513
514
  return order;
514
515
  }
@@ -3971,7 +3972,12 @@ function locateFormEnvelopePath(parsedBody) {
3971
3972
  * and swap for `${item.<field>}`. Freezing either into an opaque
3972
3973
  * `${JSON.stringify(payload.tokens)}` blob here would silently drop the
3973
3974
  * value the request depends on, breaking the fold at exactly the case an
3974
- * ARRAY/OBJECT-wrapped join field represents.
3975
+ * ARRAY/OBJECT-wrapped join field represents. A candidate whose leaves
3976
+ * include a value already registered as a payload-accessor literal (e.g. a
3977
+ * facet value a prior step derived and already wired for splicing elsewhere,
3978
+ * such as a `departmentCode` recurring inside a `tags` array) is excluded on
3979
+ * the same unconditional basis — it is already-threaded text waiting on the
3980
+ * substring-splice pass, not caller-supplied history data.
3975
3981
  *
3976
3982
  * A body with no detected form envelope worth swallowing into (a facet/
3977
3983
  * search body — all scalars, if any, sit directly at the root) still needs
@@ -3982,7 +3988,7 @@ function locateFormEnvelopePath(parsedBody) {
3982
3988
  *
3983
3989
  * Site-agnostic: operates only on the recon body's own shape.
3984
3990
  *
3985
- * A top-level-ARRAY-shaped body (e.g. a cruise-line multi-room search that
3991
+ * A top-level-ARRAY-shaped body (e.g. a travel-brand multi-room search that
3986
3992
  * batches per-room criteria as `[{...}, {...}]`) is walked element by
3987
3993
  * element, applying this same envelope logic to each object element in
3988
3994
  * textual order — otherwise a structured field living inside an array
@@ -3997,7 +4003,35 @@ function locateFormEnvelopePath(parsedBody) {
3997
4003
  * search there stops an earlier element's already-rewritten (or still-frozen)
3998
4004
  * span from swallowing a later element's identically-named key.
3999
4005
  */
4000
- function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, rootBody) {
4006
+ function isTokenBoundaryChar(ch) {
4007
+ return ch === undefined || !/[A-Za-z0-9]/.test(ch);
4008
+ }
4009
+ /**
4010
+ * True when `needle` occurs in `haystack` at a token boundary (flanked only
4011
+ * by non-alphanumeric characters or the string's own edges) — e.g. a
4012
+ * navigateTo facet's bare literal recurring inside a captured array element
4013
+ * that suffixes it with a constant delimiter string (`"<literal>;filterId=
4014
+ * urlFriendlyId"`). Exact equality alone misses this recurrence shape since
4015
+ * the leaf carries the delimiter-joined text, not the bare literal. Mirrors
4016
+ * the same boundary discipline `bindsWithoutCollision` already applies when
4017
+ * registering a value for substring splicing in the first place.
4018
+ */
4019
+ function containsValueAtTokenBoundary(haystack, needle) {
4020
+ if (needle.length === 0 || needle === haystack)
4021
+ return false;
4022
+ let from = 0;
4023
+ while (true) {
4024
+ const at = haystack.indexOf(needle, from);
4025
+ if (at === -1)
4026
+ return false;
4027
+ if (isTokenBoundaryChar(haystack[at - 1]) &&
4028
+ isTokenBoundaryChar(haystack[at + needle.length])) {
4029
+ return true;
4030
+ }
4031
+ from = at + needle.length;
4032
+ }
4033
+ }
4034
+ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, rootBody, payloadAccessorExcludePattern) {
4001
4035
  let result = template;
4002
4036
  let cursor = searchFrom;
4003
4037
  // The body root's own entries are eligible too, whenever the located
@@ -4019,7 +4053,9 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
4019
4053
  Object.keys(value).length > 0;
4020
4054
  if (!isNonEmptyArray && !isNestedObject)
4021
4055
  continue;
4022
- if (unconditionalExcludeValues.size > 0 || restrictedExcludeSourceByValue.size > 0) {
4056
+ if (unconditionalExcludeValues.size > 0 ||
4057
+ restrictedExcludeSourceByValue.size > 0 ||
4058
+ payloadAccessorExcludePattern !== null) {
4023
4059
  // A name-free join/chain value (unconditionalExcludeValues) excludes the
4024
4060
  // whole field on ANY leaf coincidence, same as before. A RESTRICTED prior-
4025
4061
  // step value (one with a real source field name) additionally requires
@@ -4030,12 +4066,39 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
4030
4066
  // count/id) merely equalling SOME other step's produced value gets the
4031
4067
  // ENTIRE array/object excluded from threading, even though nothing about
4032
4068
  // that leaf's key names the same concept as the matched value's source.
4069
+ // A registered payload-accessor literal (payloadAccessorExcludePattern)
4070
+ // excludes unconditionally too, but on a DELIMITED-SUBSTRING match, not
4071
+ // exact leaf equality — the reported shape is a facet value recurring
4072
+ // inside a captured array element with a constant suffix still attached
4073
+ // (e.g. a hash-derived facet embedded in a `tags` entry alongside a
4074
+ // trailing marker), so requiring the whole leaf to equal the bare facet
4075
+ // literal would miss exactly the case this exclusion exists for. Reuses
4076
+ // {@link buildValueAlternationPattern}'s own token-boundary rule so a
4077
+ // facet value that merely happens to be a substring of an unrelated,
4078
+ // dash/dot-concatenated compound token is still correctly left alone.
4079
+ //
4080
+ // Beyond exact equality, a leaf that carries an excluded value as a
4081
+ // token-bounded SUBSTRING (e.g. a navigateTo facet literal recurring
4082
+ // inside an array element suffixed by a constant delimiter string, like
4083
+ // `"<literal>;filterId=urlFriendlyId"`) must exclude the field too —
4084
+ // otherwise the field gets frozen wholesale before the later
4085
+ // literal-value interpolation pass (which operates on text, not parsed
4086
+ // values) ever gets a chance to splice the facet out of that element.
4033
4087
  const carriesThreadedValue = [...walkAllPrimitiveLeaves(value)].some(({ value: leaf, path: leafPath }) => {
4034
4088
  if (leaf === null)
4035
4089
  return false;
4036
4090
  const leafValue = String(leaf);
4037
4091
  if (unconditionalExcludeValues.has(leafValue))
4038
4092
  return true;
4093
+ if (payloadAccessorExcludePattern !== null) {
4094
+ payloadAccessorExcludePattern.lastIndex = 0;
4095
+ if (payloadAccessorExcludePattern.test(leafValue))
4096
+ return true;
4097
+ }
4098
+ for (const excluded of unconditionalExcludeValues) {
4099
+ if (containsValueAtTokenBoundary(leafValue, excluded))
4100
+ return true;
4101
+ }
4039
4102
  const sourceName = restrictedExcludeSourceByValue.get(leafValue);
4040
4103
  if (sourceName === undefined)
4041
4104
  return false;
@@ -4083,6 +4146,19 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
4083
4146
  }
4084
4147
  if (spanEnd === -1)
4085
4148
  continue;
4149
+ // An earlier pass in this same pipeline run (e.g. the optional-facet
4150
+ // array-element splice) may have already spliced a `${payload.<field>}`
4151
+ // accessor into this exact span — e.g. turning one element of a
4152
+ // search/filter array into `...(payload.sortOrder ? [...] : [])`. The
4153
+ // exclusion-source checks above only ever see the ORIGINAL parsed leaf
4154
+ // values, so they can't detect this; inspecting the located span's
4155
+ // CURRENT text catches it and leaves the pre-spliced accessor intact
4156
+ // instead of clobbering it with a wholesale replacement. Matching on the
4157
+ // literal template-placeholder prefix `${payload.` (rather than the bare
4158
+ // substring `payload.`) avoids false-positiving on scraped leaf data that
4159
+ // merely happens to contain the word "payload." as ordinary text.
4160
+ if (result.slice(spanStart, spanEnd).includes("${payload."))
4161
+ continue;
4086
4162
  const replacement = `$${"{"}JSON.stringify(payload.${key})${"}"}`;
4087
4163
  result = result.slice(0, spanStart) + replacement + result.slice(spanEnd);
4088
4164
  cursor = Math.max(cursor, spanStart + replacement.length);
@@ -4098,7 +4174,7 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
4098
4174
  * pass to it. Shared by the top-level-object and top-level-array branches of
4099
4175
  * {@link applyStructuredValuePayloadSubstitutions} below.
4100
4176
  */
4101
- function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom) {
4177
+ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, payloadAccessorExcludePattern) {
4102
4178
  const envelopePath = locateFormEnvelopePath(objectBody);
4103
4179
  let envelope = objectBody;
4104
4180
  for (const seg of envelopePath) {
@@ -4109,7 +4185,7 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
4109
4185
  if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) {
4110
4186
  return { result: template, nextSearchFrom: searchFrom };
4111
4187
  }
4112
- return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody);
4188
+ return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody, payloadAccessorExcludePattern);
4113
4189
  }
4114
4190
  /**
4115
4191
  * `priorStepStateBindings` carries the SAME value→binding map
@@ -4118,13 +4194,23 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
4118
4194
  * field when its own leaf's key {@link keyNamesCorrelate}s with that source
4119
4195
  * name — an unrestricted binding (name-free, e.g. a bare array-index chain
4120
4196
  * hop) or a `joinFieldValues` entry excludes unconditionally on any leaf
4121
- * match, same as before. Without the correlation requirement, an array/
4197
+ * match, same as before. A `payloadAccessorExcludeValues` key excludes
4198
+ * unconditionally too, but on a delimited-SUBSTRING match of the leaf (see
4199
+ * {@link applyStructuredValuePayloadSubstitutionsForEnvelope}), not exact
4200
+ * equality — the reported shape is a facet value recurring inside a captured
4201
+ * array element with a constant suffix still attached (e.g. a hash-derived
4202
+ * facet embedded in a `tags` entry alongside a trailing marker), so a
4203
+ * candidate field whose leaf merely CONTAINS one of those literals at a token
4204
+ * boundary is left alone here too, instead of being wholesale-swallowed into
4205
+ * an opaque `${JSON.stringify(payload.<key>)}` blob before the substring-
4206
+ * splice pass ({@link interpolateStateValues}) gets a chance to thread it.
4207
+ * Without the correlation requirement on the restricted source, an array/
4122
4208
  * object field whose own unrelated short scalar leaf (a count/id) merely
4123
4209
  * EQUALS some other step's produced value gets the whole field wrongly
4124
4210
  * excluded from threading — the same value-coincidence class of bug
4125
4211
  * {@link keyNamesCorrelate}'s other call sites already guard against.
4126
4212
  */
4127
- function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set()) {
4213
+ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set(), payloadAccessorExcludeValues = new Map()) {
4128
4214
  if (parsedBody === null || typeof parsedBody !== "object") {
4129
4215
  return template;
4130
4216
  }
@@ -4136,7 +4222,10 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
4136
4222
  else
4137
4223
  unconditionalExcludeValues.add(value);
4138
4224
  }
4139
- // A cruise-line-style multi-room/multi-guest search body commonly batches
4225
+ const payloadAccessorExcludePattern = payloadAccessorExcludeValues.size > 0
4226
+ ? buildValueAlternationPattern([...payloadAccessorExcludeValues.keys()].sort((a, b) => b.length - a.length))
4227
+ : null;
4228
+ // A travel-brand-style multi-room/multi-guest search body commonly batches
4140
4229
  // per-element criteria as a top-level JSON ARRAY rather than a single
4141
4230
  // object. Walk each element in textual order (via the growing searchFrom
4142
4231
  // cursor) instead of bailing out here — otherwise any structured field
@@ -4148,13 +4237,13 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
4148
4237
  for (const element of parsedBody) {
4149
4238
  if (element === null || typeof element !== "object" || Array.isArray(element))
4150
4239
  continue;
4151
- const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor);
4240
+ const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor, payloadAccessorExcludePattern);
4152
4241
  result = nextResult;
4153
4242
  cursor = nextSearchFrom;
4154
4243
  }
4155
4244
  return result;
4156
4245
  }
4157
- const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0);
4246
+ const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0, payloadAccessorExcludePattern);
4158
4247
  return result;
4159
4248
  }
4160
4249
  /** Maximum length to guard against indexing massive blobs (HTML fragments,
@@ -5781,6 +5870,63 @@ responseBodiesForCapacitySignal = []) {
5781
5870
  * steps declared any field to correlate against) or when a leaf's value has
5782
5871
  * no `key:value` segment matching one of them.
5783
5872
  */
5873
+ /** Yields every array-valued leaf in `value` (at any object nesting depth) —
5874
+ * `{@link applyOptionalArrayFacetSplicePayloadSubstitutions}`'s raw-array
5875
+ * search surface. Does not recurse INTO an array's own elements (a facet
5876
+ * array is always a flat array of primitives in every observed recon shape),
5877
+ * matching {@link walkAllPrimitiveLeaves}'s depth-first walk of everything
5878
+ * else. */
5879
+ function* walkArrayLeaves(value, path = []) {
5880
+ if (value === null || typeof value !== "object")
5881
+ return;
5882
+ if (Array.isArray(value)) {
5883
+ yield { value, path };
5884
+ return;
5885
+ }
5886
+ for (const [key, inner] of Object.entries(value)) {
5887
+ yield* walkArrayLeaves(inner, [...path, key]);
5888
+ }
5889
+ }
5890
+ /**
5891
+ * REST-body counterpart to {@link spliceFacetsIntoArrayVariable}'s GraphQL
5892
+ * use: a navigateTo facet's literal recurring as one array element (suffixed
5893
+ * by a constant delimiter) inside a REST JSON body gets its element text
5894
+ * swapped for `${payload.<field>}` by the plain `interpolateStateValues`
5895
+ * splice later in the pipeline — but that pass only rewrites literal text
5896
+ * IN PLACE, so when the matched facet is optional it would leave the array's
5897
+ * frozen `undefined`-shaped element (and its JSON delimiter/comma) in the
5898
+ * output whenever the caller omits the field, instead of omitting the
5899
+ * element entirely.
5900
+ *
5901
+ * This pass runs first and, for any array that matches at least one OPTIONAL
5902
+ * facet, rewrites the array's own frozen `JSON.stringify(...)` occurrence in
5903
+ * the body template to a single `${JSON.stringify([...])}` splice point
5904
+ * built from {@link spliceFacetsIntoArrayVariable}'s per-element conditional-
5905
+ * emission shape — the same `...(payload.<field> ? [...] : [])` convention
5906
+ * {@link spliceFacetsIntoStringVariable} already uses for the `key:value`
5907
+ * grammar. A required-only match (no optional facet in that array) is left
5908
+ * for the later plain splice to handle exactly as before — this pass is a
5909
+ * no-op whenever nothing it rewrites would differ, so the existing
5910
+ * required-only REST array-element shape (recon-generate-navigateto-array-
5911
+ * element-facet-splice-e2e.test.ts) never changes.
5912
+ */
5913
+ function applyOptionalArrayFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets) {
5914
+ if (!navigateToFacets.some((facet) => facet.optional))
5915
+ return template;
5916
+ if (inputBody === null || typeof inputBody !== "object")
5917
+ return template;
5918
+ let result = template;
5919
+ for (const { value } of walkArrayLeaves(inputBody)) {
5920
+ const spliced = spliceFacetsIntoArrayVariable(value, navigateToFacets);
5921
+ if (spliced === null || !spliced.includes("...(payload."))
5922
+ continue;
5923
+ const target = JSON.stringify(value);
5924
+ if (!result.includes(target))
5925
+ continue;
5926
+ result = result.split(target).join(`\${JSON.stringify(${spliced})}`);
5927
+ }
5928
+ return result;
5929
+ }
5784
5930
  function applyFacetSplicePayloadSubstitutions(template, inputBody, payloadFieldNames, optionalFieldNames) {
5785
5931
  if (payloadFieldNames.size === 0)
5786
5932
  return template;
@@ -6285,7 +6431,14 @@ flowSteps = [], vocabulary = undefined) {
6285
6431
  const appearsAnywhereInCapture = (value) => actions.some((action) => action.capture.url.includes(value) ||
6286
6432
  (action.capture.requestPostData?.includes(value) ?? false) ||
6287
6433
  Object.values(action.capture.requestHeaders).some((h) => h.includes(value)));
6288
- const unreachableNavigateToFacets = extractNavigateToFacetOrder(flowSteps).filter(({ value }) => !appearsAnywhereInCapture(value));
6434
+ // Hoisted once so both the unreachable-facet fallback above and the
6435
+ // REST array-element optional splice below (applyOptionalArrayFacetSplice
6436
+ // PayloadSubstitutions) share the exact same (value, field, optional)
6437
+ // triples — the array splice must see each binding's `optional` flag
6438
+ // precisely as the flow declared it, same as the already-fixed GraphQL
6439
+ // array-variable path ({@link spliceFacetsIntoArrayVariable}).
6440
+ const navigateToFacetOrder = extractNavigateToFacetOrder(flowSteps);
6441
+ const unreachableNavigateToFacets = navigateToFacetOrder.filter(({ value }) => !appearsAnywhereInCapture(value));
6289
6442
  if (unreachableNavigateToFacets.length > 0) {
6290
6443
  const correlated = correlateUnreachableNavigateToFacets(unreachableNavigateToFacets, actions);
6291
6444
  for (const { field } of correlated === null ? [] : unreachableNavigateToFacets) {
@@ -6554,6 +6707,16 @@ flowSteps = [], vocabulary = undefined) {
6554
6707
  const rawBodyWithFacetSplices = parsedBody !== null
6555
6708
  ? applyFacetSplicePayloadSubstitutions(rawBodyWithFormSubs, parsedBody, restPayloadFieldNames, restOptionalFieldNames)
6556
6709
  : rawBodyWithFormSubs;
6710
+ // Optional-facet array-element splice — a navigateTo facet literal
6711
+ // recurring as one element of an array field (not the `key:value`
6712
+ // grammar above) must drop its own element, not freeze an `undefined`,
6713
+ // when its binding is optional and the caller omits it. Runs BEFORE
6714
+ // Mechanism B (below) can swallow the whole array wholesale as an
6715
+ // unparameterized structured value — see
6716
+ // {@link applyOptionalArrayFacetSplicePayloadSubstitutions}.
6717
+ const rawBodyWithOptionalArrayFacetSplices = parsedBody !== null
6718
+ ? applyOptionalArrayFacetSplicePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, navigateToFacetOrder)
6719
+ : rawBodyWithFacetSplices;
6557
6720
  // Mechanism B — parameterize whole nested caller structures
6558
6721
  // (experienceData/educationData history, opaque eventData) BEFORE value
6559
6722
  // substitution reaches inside them: swallowing the entire array/object first
@@ -6564,7 +6727,7 @@ flowSteps = [], vocabulary = undefined) {
6564
6727
  // dependent-drill-down join field (e.g. a bulk `{"tokens":[...]}` lookup)
6565
6728
  // must stay reachable for state threading, not get frozen as caller data.
6566
6729
  const rawBodyWithStructuredSubs = parsedBody !== null
6567
- ? applyStructuredValuePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, outStructuredKeys, deriveStateVarByValue(prior, cap), joinFieldValuesByStep.get(i) ?? new Set())
6730
+ ? applyStructuredValuePayloadSubstitutions(rawBodyWithOptionalArrayFacetSplices, parsedBody, outStructuredKeys, deriveStateVarByValue(prior, cap), joinFieldValuesByStep.get(i) ?? new Set(), payloadAccessorByValue)
6568
6731
  : rawBodyWithFormSubs;
6569
6732
  // Whole-value caller coordinates bind here — after structured subs, BEFORE
6570
6733
  // state threading — so a composite coordinate (a jobLocation or jobSeqNo
@@ -7487,6 +7650,50 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
7487
7650
  }
7488
7651
  return `[${elements.join(", ")}].join("")`;
7489
7652
  }
7653
+ /**
7654
+ * Splices a captured array-valued GraphQL variable's elements against
7655
+ * navigateTo facet bindings instead of the `key:value` segment grammar
7656
+ * {@link spliceFacetsIntoStringVariable} matches — an array element like
7657
+ * `widget-x;filterId=urlFriendlyId` carries a bare literal plus a constant
7658
+ * trailing delimiter, with no `key:` prefix to key off of. Matching is
7659
+ * token-boundary guarded the same way {@link bindsWithoutCollision} (recon-
7660
+ * generate.ts ~7223) guards persona bindings elsewhere in this file, so a
7661
+ * facet literal that's merely a substring of an unrelated element never
7662
+ * misfires. A matched binding whose `optional` flag is set is emitted as a
7663
+ * conditionally-included array element (`...(payload.<field> ? [...] : [])`)
7664
+ * instead of inline, the same optional-splice convention
7665
+ * {@link spliceFacetsIntoStringVariable} uses, so an absent optional facet
7666
+ * drops its element rather than emitting `undefined`.
7667
+ */
7668
+ function spliceFacetsIntoArrayVariable(value, facets) {
7669
+ if (!Array.isArray(value))
7670
+ return null;
7671
+ const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
7672
+ const matchFacet = (element) => facets.find(({ value: literal }) => {
7673
+ if (element === literal)
7674
+ return true;
7675
+ if (!element.startsWith(literal))
7676
+ return false;
7677
+ return !isAlnum(element[literal.length]);
7678
+ });
7679
+ let matchCount = 0;
7680
+ const elements = value.map((element) => {
7681
+ if (typeof element !== "string")
7682
+ return JSON.stringify(element);
7683
+ const matched = matchFacet(element);
7684
+ if (!matched)
7685
+ return JSON.stringify(element);
7686
+ matchCount++;
7687
+ const suffix = element.slice(matched.value.length);
7688
+ const elementLiteral = `\`\${payload.${matched.field}}${escapeForTemplateLiteral(suffix)}\``;
7689
+ return matched.optional
7690
+ ? `...(payload.${matched.field} ? [${elementLiteral}] : [])`
7691
+ : elementLiteral;
7692
+ });
7693
+ if (matchCount === 0)
7694
+ return null;
7695
+ return `[${elements.join(", ")}]`;
7696
+ }
7490
7697
  /**
7491
7698
  * Renders the variables literal for the primary-operation getGql() call —
7492
7699
  * each key from the selected capture's own recorded variables is bound to
@@ -7498,9 +7705,12 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
7498
7705
  * other value is emitted verbatim via JSON.stringify. `optionalFieldNames`
7499
7706
  * marks which of `payloadFieldNames` a facet-string splice should treat as
7500
7707
  * optional (see {@link spliceFacetsIntoStringVariable}); it has no effect on
7501
- * top-level key correlation.
7708
+ * top-level key correlation. When a value is an array (not itself a string
7709
+ * key correlation candidate), its elements are matched against
7710
+ * `navigateToFacets` instead — see {@link spliceFacetsIntoArrayVariable} for
7711
+ * why that's a distinct grammar from the `key:value` one above.
7502
7712
  */
7503
- function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames = new Set()) {
7713
+ function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames = new Set(), navigateToFacets = []) {
7504
7714
  if (variables === null || typeof variables !== "object" || Array.isArray(variables))
7505
7715
  return "{}";
7506
7716
  const fields = payloadFieldNames ? [...payloadFieldNames] : [];
@@ -7508,7 +7718,8 @@ function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames
7508
7718
  const matchedField = fields.find((field) => field.toLowerCase() === key.toLowerCase());
7509
7719
  const facetSpliceExpr = matchedField
7510
7720
  ? null
7511
- : spliceFacetsIntoStringVariable(value, fields, optionalFieldNames);
7721
+ : (spliceFacetsIntoStringVariable(value, fields, optionalFieldNames) ??
7722
+ spliceFacetsIntoArrayVariable(value, navigateToFacets));
7512
7723
  const valueExpr = matchedField
7513
7724
  ? `payload.${matchedField}`
7514
7725
  : (facetSpliceExpr ?? JSON.stringify(value));
@@ -7945,7 +8156,7 @@ function isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, sibl
7945
8156
  * iteration's request, exactly the defect described in
7946
8157
  * docs/recon-generate-nested-fold-flatmaps-away-the-parent-so-drill-params-freeze.md
7947
8158
  * (a `packageCode`/`groupId`/`sailDate` triple that provably varies per
7948
- * cruise, silently frozen because no threaded field explained it). Path
8159
+ * voyage, silently frozen because no threaded field explained it). Path
7949
8160
  * segments are deliberately not checked: {@link endpointKey} requires an
7950
8161
  * identical pathname to group two captures at all, so no path segment can
7951
8162
  * ever be observed to vary within a matched group.
@@ -10522,7 +10733,7 @@ function buildContractChecklist(opts) {
10522
10733
  /** Generates a complete contract.ts source string for a plugin — exported so
10523
10734
  * unit tests can drive the emitter directly without spawning the CLI. */
10524
10735
  function emitContractTs(opts) {
10525
- 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;
10736
+ 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(), flowSteps: contractFlowSteps = [], headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
10526
10737
  const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
10527
10738
  /** Single source of truth for every GraphQL-emission decision below
10528
10739
  * (clientImport, gqlCacheBlock, queryConst, and both executeHttpBody
@@ -11032,10 +11243,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
11032
11243
  // the same captured request-variables object the signal was itself
11033
11244
  // detected from (see paginationOperationIdentity above) — the default
11034
11245
  // `{ q: payload.query }` REST body has no skip/count container to advance.
11246
+ const navigateToFacets = extractNavigateToFacetOrder(contractFlowSteps);
11035
11247
  const gqlVariablesExpr = gqlOperationName
11036
- ? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
11248
+ ? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
11037
11249
  : paginationSignal
11038
- ? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
11250
+ ? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
11039
11251
  : "{ q: payload.query }";
11040
11252
  /** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
11041
11253
  * — that fold every resolved plan's drill-down data onto `dataVarName`'s
@@ -12906,6 +13118,7 @@ async function main() {
12906
13118
  isSubmissionFlow,
12907
13119
  inputBody,
12908
13120
  hasMultipartStep,
13121
+ flowSteps,
12909
13122
  actionSteps,
12910
13123
  foldReturnSpec,
12911
13124
  discoveredFormFields,