@enricai/barnacle 1.12.76 → 1.12.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scraper/flow-runner.d.ts +13 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +114 -24
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +24 -6
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +87 -19
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +4 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +5 -5
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +103 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +857 -117
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +2 -2
|
@@ -62,10 +62,14 @@ exports.ancestorScopedAccessor = ancestorScopedAccessor;
|
|
|
62
62
|
exports.compileActionSteps = compileActionSteps;
|
|
63
63
|
exports.collectHeaderBindings = collectHeaderBindings;
|
|
64
64
|
exports.deriveProducerBoundaryBindings = deriveProducerBoundaryBindings;
|
|
65
|
+
exports.applyPayloadKeyValueSubstitutions = applyPayloadKeyValueSubstitutions;
|
|
65
66
|
exports.collectUnboundLiterals = collectUnboundLiterals;
|
|
67
|
+
exports.resolveRecurringNavigateToFacets = resolveRecurringNavigateToFacets;
|
|
66
68
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
67
69
|
exports.countSpliceableFacets = countSpliceableFacets;
|
|
68
70
|
exports.spliceFacetsIntoStringVariable = spliceFacetsIntoStringVariable;
|
|
71
|
+
exports.spliceFacetsIntoArrayVariable = spliceFacetsIntoArrayVariable;
|
|
72
|
+
exports.renderGqlVariablesExpr = renderGqlVariablesExpr;
|
|
69
73
|
exports.findAllObjectArrayFields = findAllObjectArrayFields;
|
|
70
74
|
exports.findAllObjectArrayFieldsOrWholeObject = findAllObjectArrayFieldsOrWholeObject;
|
|
71
75
|
exports.applyDrillParamBindings = applyDrillParamBindings;
|
|
@@ -381,14 +385,53 @@ function extractNavigateToHashFragmentValue(url, previousHash) {
|
|
|
381
385
|
if (hashIndex === -1)
|
|
382
386
|
return null;
|
|
383
387
|
const hash = url.slice(hashIndex + 1);
|
|
384
|
-
if (previousHash
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
388
|
+
if (previousHash) {
|
|
389
|
+
// A growing hash can add more than one token per step (a "label/value"
|
|
390
|
+
// pair, or a "key=value" pair), at either end, joined by any hash
|
|
391
|
+
// separator. The facet's actual VALUE is the last token that is new
|
|
392
|
+
// relative to the previous hash, with any "key=" prefix removed.
|
|
393
|
+
const previousTokens = new Set(splitHashTokens(previousHash));
|
|
394
|
+
const added = splitHashTokens(hash).filter((token) => !previousTokens.has(token));
|
|
395
|
+
const delta = added[added.length - 1];
|
|
396
|
+
if (delta !== undefined)
|
|
397
|
+
return stripHashKeyPrefix(delta);
|
|
398
|
+
}
|
|
399
|
+
const tokens = splitHashTokens(hash);
|
|
400
|
+
return tokens.length > 0 ? stripHashKeyPrefix(tokens[tokens.length - 1]) : null;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Single walk over navigateTo+payloadField steps shared by
|
|
404
|
+
* {@link harvestPersonaBindings} and {@link extractNavigateToFacetOrder}, so
|
|
405
|
+
* both derive identical literals. The delta baseline is the most recent
|
|
406
|
+
* hash-bearing navigateTo; a hashless reset navigation must not erase it, or
|
|
407
|
+
* the next cumulative hash would be taken whole and never recur in a request.
|
|
408
|
+
*/
|
|
409
|
+
function walkNavigateToFacets(flowSteps) {
|
|
410
|
+
const facets = [];
|
|
411
|
+
let baselineHash;
|
|
412
|
+
for (const step of flowSteps) {
|
|
413
|
+
if (typeof step === "string" || step.navigateTo === undefined)
|
|
414
|
+
continue;
|
|
415
|
+
const hashIndex = step.navigateTo.indexOf("#");
|
|
416
|
+
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
417
|
+
if (step.payloadField) {
|
|
418
|
+
facets.push({
|
|
419
|
+
value: extractNavigateToHashFragmentValue(step.navigateTo, baselineHash),
|
|
420
|
+
field: step.payloadField,
|
|
421
|
+
optional: step.optional === true,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (currentHash !== undefined)
|
|
425
|
+
baselineHash = currentHash;
|
|
389
426
|
}
|
|
390
|
-
|
|
391
|
-
|
|
427
|
+
return facets;
|
|
428
|
+
}
|
|
429
|
+
function splitHashTokens(hash) {
|
|
430
|
+
return hash.split(/[,/;&]/).filter((token) => token.length > 0);
|
|
431
|
+
}
|
|
432
|
+
function stripHashKeyPrefix(token) {
|
|
433
|
+
const stripped = token.slice(token.indexOf("=") + 1);
|
|
434
|
+
return stripped.length > 0 ? stripped : token;
|
|
392
435
|
}
|
|
393
436
|
/**
|
|
394
437
|
* Derives a payload field name from the field LABEL in a fill/enter/type
|
|
@@ -436,7 +479,11 @@ function deriveFillLabelField(instruction) {
|
|
|
436
479
|
function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
437
480
|
const bindings = new Map();
|
|
438
481
|
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
439
|
-
|
|
482
|
+
for (const facet of walkNavigateToFacets(flowSteps)) {
|
|
483
|
+
if (facet.value !== null && !bindings.has(facet.value)) {
|
|
484
|
+
bindings.set(facet.value, `payload.${facet.field}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
440
487
|
for (const step of flowSteps) {
|
|
441
488
|
const isObj = typeof step !== "string";
|
|
442
489
|
const instruction = isObj ? step.step : step;
|
|
@@ -447,21 +494,8 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
447
494
|
// `.../#/jobs/category-widgets`), so the value to correlate against
|
|
448
495
|
// downstream header/body templates is the URL's own hash-derived
|
|
449
496
|
// fragment, not anything extractStepPersonaValue could find in `step.step`.
|
|
450
|
-
if (isObj && step.navigateTo !== undefined)
|
|
451
|
-
const hashIndex = step.navigateTo.indexOf("#");
|
|
452
|
-
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
453
|
-
if (!step.payloadField) {
|
|
454
|
-
previousNavigateToHash = currentHash;
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
const value = extractNavigateToHashFragmentValue(step.navigateTo, previousNavigateToHash);
|
|
458
|
-
previousNavigateToHash = currentHash;
|
|
459
|
-
if (value === null)
|
|
460
|
-
continue;
|
|
461
|
-
if (!bindings.has(value))
|
|
462
|
-
bindings.set(value, `payload.${step.payloadField}`);
|
|
497
|
+
if (isObj && step.navigateTo !== undefined)
|
|
463
498
|
continue;
|
|
464
|
-
}
|
|
465
499
|
const vocabField = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
466
500
|
// Vocabulary wins outright. Only on a miss do we fall back to deriving the
|
|
467
501
|
// field from the instruction's own label — and never when the author opted
|
|
@@ -491,26 +525,18 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
491
525
|
* which DOES see the captures, re-derive the same (value, field) pairs and
|
|
492
526
|
* try the causally-adjacent-action fallback (see `emitMultiStepExecuteHttp`)
|
|
493
527
|
* for whichever ones the recurrence-anchored path left unbound.
|
|
528
|
+
*
|
|
529
|
+
* A step whose URL carries no hash literal still holds its slot (empty
|
|
530
|
+
* `value`) so positional correlation against captured transitions stays
|
|
531
|
+
* aligned with the declared facets; {@link resolveRecurringNavigateToFacets}
|
|
532
|
+
* drops the empty literal itself and keeps only a correlated value.
|
|
494
533
|
*/
|
|
495
534
|
function extractNavigateToFacetOrder(flowSteps) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const hashIndex = step.navigateTo.indexOf("#");
|
|
502
|
-
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
503
|
-
if (!step.payloadField) {
|
|
504
|
-
previousNavigateToHash = currentHash;
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
const value = extractNavigateToHashFragmentValue(step.navigateTo, previousNavigateToHash);
|
|
508
|
-
previousNavigateToHash = currentHash;
|
|
509
|
-
if (value === null || value.length === 0)
|
|
510
|
-
continue;
|
|
511
|
-
order.push({ value, field: step.payloadField });
|
|
512
|
-
}
|
|
513
|
-
return order;
|
|
535
|
+
return walkNavigateToFacets(flowSteps).map((facet) => ({
|
|
536
|
+
value: facet.value ?? "",
|
|
537
|
+
field: facet.field,
|
|
538
|
+
optional: facet.optional,
|
|
539
|
+
}));
|
|
514
540
|
}
|
|
515
541
|
/**
|
|
516
542
|
* Detects a flow step whose quoted VALUE is the space-joined concatenation of
|
|
@@ -2823,6 +2849,17 @@ function jsonBodyLeafValues(requestPostData) {
|
|
|
2823
2849
|
}
|
|
2824
2850
|
return values;
|
|
2825
2851
|
}
|
|
2852
|
+
/** Parses a captured request body as JSON; null for an absent or non-JSON body (multipart raw bytes). */
|
|
2853
|
+
function parseJsonBodyOrNull(requestPostData) {
|
|
2854
|
+
if (typeof requestPostData !== "string" || requestPostData.length === 0)
|
|
2855
|
+
return null;
|
|
2856
|
+
try {
|
|
2857
|
+
return JSON.parse(requestPostData);
|
|
2858
|
+
}
|
|
2859
|
+
catch {
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2826
2863
|
/**
|
|
2827
2864
|
* Same JSON body walk as {@link jsonBodyLeafValues}, but grouped by the JSON
|
|
2828
2865
|
* key/array-index that carries each leaf value — the by-name correlation
|
|
@@ -3971,7 +4008,12 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3971
4008
|
* and swap for `${item.<field>}`. Freezing either into an opaque
|
|
3972
4009
|
* `${JSON.stringify(payload.tokens)}` blob here would silently drop the
|
|
3973
4010
|
* value the request depends on, breaking the fold at exactly the case an
|
|
3974
|
-
* ARRAY/OBJECT-wrapped join field represents.
|
|
4011
|
+
* ARRAY/OBJECT-wrapped join field represents. A candidate whose leaves
|
|
4012
|
+
* include a value already registered as a payload-accessor literal (e.g. a
|
|
4013
|
+
* facet value a prior step derived and already wired for splicing elsewhere,
|
|
4014
|
+
* such as a `departmentCode` recurring inside a `tags` array) is excluded on
|
|
4015
|
+
* the same unconditional basis — it is already-threaded text waiting on the
|
|
4016
|
+
* substring-splice pass, not caller-supplied history data.
|
|
3975
4017
|
*
|
|
3976
4018
|
* A body with no detected form envelope worth swallowing into (a facet/
|
|
3977
4019
|
* search body — all scalars, if any, sit directly at the root) still needs
|
|
@@ -3982,7 +4024,7 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3982
4024
|
*
|
|
3983
4025
|
* Site-agnostic: operates only on the recon body's own shape.
|
|
3984
4026
|
*
|
|
3985
|
-
* A top-level-ARRAY-shaped body (e.g. a
|
|
4027
|
+
* A top-level-ARRAY-shaped body (e.g. a travel-brand multi-room search that
|
|
3986
4028
|
* batches per-room criteria as `[{...}, {...}]`) is walked element by
|
|
3987
4029
|
* element, applying this same envelope logic to each object element in
|
|
3988
4030
|
* textual order — otherwise a structured field living inside an array
|
|
@@ -3997,7 +4039,56 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3997
4039
|
* search there stops an earlier element's already-rewritten (or still-frozen)
|
|
3998
4040
|
* span from swallowing a later element's identically-named key.
|
|
3999
4041
|
*/
|
|
4000
|
-
function
|
|
4042
|
+
function isTokenBoundaryChar(ch) {
|
|
4043
|
+
return ch === undefined || !/[A-Za-z0-9]/.test(ch);
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* True when `needle` occurs in `haystack` at a token boundary (flanked only
|
|
4047
|
+
* by non-alphanumeric characters or the string's own edges) — e.g. a
|
|
4048
|
+
* navigateTo facet's bare literal recurring inside a captured array element
|
|
4049
|
+
* that suffixes it with a constant delimiter string (`"<literal>;filterId=
|
|
4050
|
+
* urlFriendlyId"`). Exact equality alone misses this recurrence shape since
|
|
4051
|
+
* the leaf carries the delimiter-joined text, not the bare literal. Mirrors
|
|
4052
|
+
* the same boundary discipline `bindsWithoutCollision` already applies when
|
|
4053
|
+
* registering a value for substring splicing in the first place.
|
|
4054
|
+
*/
|
|
4055
|
+
function containsValueAtTokenBoundary(haystack, needle) {
|
|
4056
|
+
if (needle.length === 0 || needle === haystack)
|
|
4057
|
+
return false;
|
|
4058
|
+
let from = 0;
|
|
4059
|
+
while (true) {
|
|
4060
|
+
const at = haystack.indexOf(needle, from);
|
|
4061
|
+
if (at === -1)
|
|
4062
|
+
return false;
|
|
4063
|
+
if (isTokenBoundaryChar(haystack[at - 1]) &&
|
|
4064
|
+
isTokenBoundaryChar(haystack[at + needle.length])) {
|
|
4065
|
+
return true;
|
|
4066
|
+
}
|
|
4067
|
+
from = at + needle.length;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
const structuredFieldSamples = new WeakMap();
|
|
4071
|
+
/**
|
|
4072
|
+
* Single registration point for a structured (array/object) payload field, so
|
|
4073
|
+
* the declared schema type is inferred from every captured body carrying the
|
|
4074
|
+
* field rather than from whichever body or visiting order reached it first.
|
|
4075
|
+
* Both the by-index accessor loop and the whole-field substitution pass go
|
|
4076
|
+
* through here, so an indexed accessor can never coexist with a string schema.
|
|
4077
|
+
*/
|
|
4078
|
+
function registerStructuredPayloadField(outStructuredKeys, field, value) {
|
|
4079
|
+
if (value === undefined || value === null)
|
|
4080
|
+
return;
|
|
4081
|
+
const samplesByField = structuredFieldSamples.get(outStructuredKeys) ?? new Map();
|
|
4082
|
+
structuredFieldSamples.set(outStructuredKeys, samplesByField);
|
|
4083
|
+
const samples = samplesByField.get(field) ?? [];
|
|
4084
|
+
samplesByField.set(field, samples);
|
|
4085
|
+
const serialized = JSON.stringify(value);
|
|
4086
|
+
if (samples.some((sample) => JSON.stringify(sample) === serialized))
|
|
4087
|
+
return;
|
|
4088
|
+
samples.push(value);
|
|
4089
|
+
outStructuredKeys.set(field, inferZodSchemaFromSamples(samples));
|
|
4090
|
+
}
|
|
4091
|
+
function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, rootBody, payloadAccessorExcludePattern, ownedArrayPathKeys) {
|
|
4001
4092
|
let result = template;
|
|
4002
4093
|
let cursor = searchFrom;
|
|
4003
4094
|
// The body root's own entries are eligible too, whenever the located
|
|
@@ -4007,11 +4098,11 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4007
4098
|
// pagination/sort block) outranks the root as the "form envelope". Root
|
|
4008
4099
|
// keys already present on the envelope are skipped so nothing is visited
|
|
4009
4100
|
// (or registered) twice.
|
|
4010
|
-
const envelopeEntries = Object.entries(envelope);
|
|
4011
|
-
const rootEntries = envelopePath.length === 0
|
|
4101
|
+
const envelopeEntries = Object.entries(envelope).map(([key, value]) => [key, value, [...envelopePath, key]]);
|
|
4102
|
+
const rootEntries = (envelopePath.length === 0
|
|
4012
4103
|
? []
|
|
4013
|
-
: Object.entries(rootBody).filter(([key]) => key !== envelopePath[0] && !(key in envelope));
|
|
4014
|
-
for (const [key, value] of [...envelopeEntries, ...rootEntries]) {
|
|
4104
|
+
: Object.entries(rootBody).filter(([key]) => key !== envelopePath[0] && !(key in envelope))).map(([key, value]) => [key, value, [key]]);
|
|
4105
|
+
for (const [key, value, valuePath] of [...envelopeEntries, ...rootEntries]) {
|
|
4015
4106
|
const isNonEmptyArray = Array.isArray(value) && value.length > 0;
|
|
4016
4107
|
const isNestedObject = value !== null &&
|
|
4017
4108
|
typeof value === "object" &&
|
|
@@ -4019,7 +4110,25 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4019
4110
|
Object.keys(value).length > 0;
|
|
4020
4111
|
if (!isNonEmptyArray && !isNestedObject)
|
|
4021
4112
|
continue;
|
|
4022
|
-
|
|
4113
|
+
// An array path the facet pass owns is rendered element by element at every
|
|
4114
|
+
// call site, so it is neither a payload field of its own nor swallowable.
|
|
4115
|
+
if (isNonEmptyArray && ownedArrayPathKeys.has(arrayPathKey(valuePath)))
|
|
4116
|
+
continue;
|
|
4117
|
+
// Schema registration and the wholesale text-swallow below are
|
|
4118
|
+
// independent concerns: this field's inferred Zod shape is correct
|
|
4119
|
+
// regardless of whether any of the exclusion/guard checks below skip
|
|
4120
|
+
// freezing its literal into the template. Registering it here (before
|
|
4121
|
+
// those checks, and before even locating the span) keeps the generated
|
|
4122
|
+
// contract's declared type in sync with payloadAccessorForPath's
|
|
4123
|
+
// array/object accessors even when the wholesale swallow is skipped.
|
|
4124
|
+
registerStructuredPayloadField(outStructuredKeys, key, value);
|
|
4125
|
+
// A call site whose array carries a facet was already rewritten by the
|
|
4126
|
+
// facet pass (the span-text guard below leaves it intact); a site whose
|
|
4127
|
+
// array carries none falls through to the same opaque `payload.<key>`
|
|
4128
|
+
// passthrough every non-facet array gets, so the declared field is used.
|
|
4129
|
+
if (unconditionalExcludeValues.size > 0 ||
|
|
4130
|
+
restrictedExcludeSourceByValue.size > 0 ||
|
|
4131
|
+
payloadAccessorExcludePattern !== null) {
|
|
4023
4132
|
// A name-free join/chain value (unconditionalExcludeValues) excludes the
|
|
4024
4133
|
// whole field on ANY leaf coincidence, same as before. A RESTRICTED prior-
|
|
4025
4134
|
// step value (one with a real source field name) additionally requires
|
|
@@ -4030,12 +4139,39 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4030
4139
|
// count/id) merely equalling SOME other step's produced value gets the
|
|
4031
4140
|
// ENTIRE array/object excluded from threading, even though nothing about
|
|
4032
4141
|
// that leaf's key names the same concept as the matched value's source.
|
|
4142
|
+
// A registered payload-accessor literal (payloadAccessorExcludePattern)
|
|
4143
|
+
// excludes unconditionally too, but on a DELIMITED-SUBSTRING match, not
|
|
4144
|
+
// exact leaf equality — the reported shape is a facet value recurring
|
|
4145
|
+
// inside a captured array element with a constant suffix still attached
|
|
4146
|
+
// (e.g. a hash-derived facet embedded in a `tags` entry alongside a
|
|
4147
|
+
// trailing marker), so requiring the whole leaf to equal the bare facet
|
|
4148
|
+
// literal would miss exactly the case this exclusion exists for. Reuses
|
|
4149
|
+
// {@link buildValueAlternationPattern}'s own token-boundary rule so a
|
|
4150
|
+
// facet value that merely happens to be a substring of an unrelated,
|
|
4151
|
+
// dash/dot-concatenated compound token is still correctly left alone.
|
|
4152
|
+
//
|
|
4153
|
+
// Beyond exact equality, a leaf that carries an excluded value as a
|
|
4154
|
+
// token-bounded SUBSTRING (e.g. a navigateTo facet literal recurring
|
|
4155
|
+
// inside an array element suffixed by a constant delimiter string, like
|
|
4156
|
+
// `"<literal>;filterId=urlFriendlyId"`) must exclude the field too —
|
|
4157
|
+
// otherwise the field gets frozen wholesale before the later
|
|
4158
|
+
// literal-value interpolation pass (which operates on text, not parsed
|
|
4159
|
+
// values) ever gets a chance to splice the facet out of that element.
|
|
4033
4160
|
const carriesThreadedValue = [...walkAllPrimitiveLeaves(value)].some(({ value: leaf, path: leafPath }) => {
|
|
4034
4161
|
if (leaf === null)
|
|
4035
4162
|
return false;
|
|
4036
4163
|
const leafValue = String(leaf);
|
|
4037
4164
|
if (unconditionalExcludeValues.has(leafValue))
|
|
4038
4165
|
return true;
|
|
4166
|
+
if (payloadAccessorExcludePattern !== null) {
|
|
4167
|
+
payloadAccessorExcludePattern.lastIndex = 0;
|
|
4168
|
+
if (payloadAccessorExcludePattern.test(leafValue))
|
|
4169
|
+
return true;
|
|
4170
|
+
}
|
|
4171
|
+
for (const excluded of unconditionalExcludeValues) {
|
|
4172
|
+
if (containsValueAtTokenBoundary(leafValue, excluded))
|
|
4173
|
+
return true;
|
|
4174
|
+
}
|
|
4039
4175
|
const sourceName = restrictedExcludeSourceByValue.get(leafValue);
|
|
4040
4176
|
if (sourceName === undefined)
|
|
4041
4177
|
return false;
|
|
@@ -4083,12 +4219,22 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4083
4219
|
}
|
|
4084
4220
|
if (spanEnd === -1)
|
|
4085
4221
|
continue;
|
|
4222
|
+
// An earlier pass in this same pipeline run (e.g. the optional-facet
|
|
4223
|
+
// array-element splice) may have already spliced a `${payload.<field>}`
|
|
4224
|
+
// accessor into this exact span — e.g. turning one element of a
|
|
4225
|
+
// search/filter array into `...(payload.sortOrder ? [...] : [])`. The
|
|
4226
|
+
// exclusion-source checks above only ever see the ORIGINAL parsed leaf
|
|
4227
|
+
// values, so they can't detect this; inspecting the located span's
|
|
4228
|
+
// CURRENT text catches it and leaves the pre-spliced accessor intact
|
|
4229
|
+
// instead of clobbering it with a wholesale replacement. Matching on the
|
|
4230
|
+
// literal template-placeholder prefix `${payload.` (rather than the bare
|
|
4231
|
+
// substring `payload.`) avoids false-positiving on scraped leaf data that
|
|
4232
|
+
// merely happens to contain the word "payload." as ordinary text.
|
|
4233
|
+
if (result.slice(spanStart, spanEnd).includes("${payload."))
|
|
4234
|
+
continue;
|
|
4086
4235
|
const replacement = `$${"{"}JSON.stringify(payload.${key})${"}"}`;
|
|
4087
4236
|
result = result.slice(0, spanStart) + replacement + result.slice(spanEnd);
|
|
4088
4237
|
cursor = Math.max(cursor, spanStart + replacement.length);
|
|
4089
|
-
if (!outStructuredKeys.has(key)) {
|
|
4090
|
-
outStructuredKeys.set(key, inferZodSchema(value));
|
|
4091
|
-
}
|
|
4092
4238
|
}
|
|
4093
4239
|
return { result, nextSearchFrom: cursor };
|
|
4094
4240
|
}
|
|
@@ -4098,7 +4244,7 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4098
4244
|
* pass to it. Shared by the top-level-object and top-level-array branches of
|
|
4099
4245
|
* {@link applyStructuredValuePayloadSubstitutions} below.
|
|
4100
4246
|
*/
|
|
4101
|
-
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom) {
|
|
4247
|
+
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, payloadAccessorExcludePattern, ownedArrayPathKeys) {
|
|
4102
4248
|
const envelopePath = locateFormEnvelopePath(objectBody);
|
|
4103
4249
|
let envelope = objectBody;
|
|
4104
4250
|
for (const seg of envelopePath) {
|
|
@@ -4109,7 +4255,7 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4109
4255
|
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
4110
4256
|
return { result: template, nextSearchFrom: searchFrom };
|
|
4111
4257
|
}
|
|
4112
|
-
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody);
|
|
4258
|
+
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody, payloadAccessorExcludePattern, ownedArrayPathKeys);
|
|
4113
4259
|
}
|
|
4114
4260
|
/**
|
|
4115
4261
|
* `priorStepStateBindings` carries the SAME value→binding map
|
|
@@ -4118,13 +4264,23 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4118
4264
|
* field when its own leaf's key {@link keyNamesCorrelate}s with that source
|
|
4119
4265
|
* name — an unrestricted binding (name-free, e.g. a bare array-index chain
|
|
4120
4266
|
* hop) or a `joinFieldValues` entry excludes unconditionally on any leaf
|
|
4121
|
-
* match, same as before.
|
|
4267
|
+
* match, same as before. A `payloadAccessorExcludeValues` key excludes
|
|
4268
|
+
* unconditionally too, but on a delimited-SUBSTRING match of the leaf (see
|
|
4269
|
+
* {@link applyStructuredValuePayloadSubstitutionsForEnvelope}), not exact
|
|
4270
|
+
* equality — the reported shape is a facet value recurring inside a captured
|
|
4271
|
+
* array element with a constant suffix still attached (e.g. a hash-derived
|
|
4272
|
+
* facet embedded in a `tags` entry alongside a trailing marker), so a
|
|
4273
|
+
* candidate field whose leaf merely CONTAINS one of those literals at a token
|
|
4274
|
+
* boundary is left alone here too, instead of being wholesale-swallowed into
|
|
4275
|
+
* an opaque `${JSON.stringify(payload.<key>)}` blob before the substring-
|
|
4276
|
+
* splice pass ({@link interpolateStateValues}) gets a chance to thread it.
|
|
4277
|
+
* Without the correlation requirement on the restricted source, an array/
|
|
4122
4278
|
* object field whose own unrelated short scalar leaf (a count/id) merely
|
|
4123
4279
|
* EQUALS some other step's produced value gets the whole field wrongly
|
|
4124
4280
|
* excluded from threading — the same value-coincidence class of bug
|
|
4125
4281
|
* {@link keyNamesCorrelate}'s other call sites already guard against.
|
|
4126
4282
|
*/
|
|
4127
|
-
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set()) {
|
|
4283
|
+
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set(), payloadAccessorExcludeValues = new Map(), ownedArrayPathKeys = new Set()) {
|
|
4128
4284
|
if (parsedBody === null || typeof parsedBody !== "object") {
|
|
4129
4285
|
return template;
|
|
4130
4286
|
}
|
|
@@ -4136,7 +4292,10 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
|
|
|
4136
4292
|
else
|
|
4137
4293
|
unconditionalExcludeValues.add(value);
|
|
4138
4294
|
}
|
|
4139
|
-
|
|
4295
|
+
const payloadAccessorExcludePattern = payloadAccessorExcludeValues.size > 0
|
|
4296
|
+
? buildValueAlternationPattern([...payloadAccessorExcludeValues.keys()].sort((a, b) => b.length - a.length))
|
|
4297
|
+
: null;
|
|
4298
|
+
// A travel-brand-style multi-room/multi-guest search body commonly batches
|
|
4140
4299
|
// per-element criteria as a top-level JSON ARRAY rather than a single
|
|
4141
4300
|
// object. Walk each element in textual order (via the growing searchFrom
|
|
4142
4301
|
// cursor) instead of bailing out here — otherwise any structured field
|
|
@@ -4148,13 +4307,13 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
|
|
|
4148
4307
|
for (const element of parsedBody) {
|
|
4149
4308
|
if (element === null || typeof element !== "object" || Array.isArray(element))
|
|
4150
4309
|
continue;
|
|
4151
|
-
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor);
|
|
4310
|
+
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor, payloadAccessorExcludePattern, ownedArrayPathKeys);
|
|
4152
4311
|
result = nextResult;
|
|
4153
4312
|
cursor = nextSearchFrom;
|
|
4154
4313
|
}
|
|
4155
4314
|
return result;
|
|
4156
4315
|
}
|
|
4157
|
-
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0);
|
|
4316
|
+
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0, payloadAccessorExcludePattern, ownedArrayPathKeys);
|
|
4158
4317
|
return result;
|
|
4159
4318
|
}
|
|
4160
4319
|
/** Maximum length to guard against indexing massive blobs (HTML fragments,
|
|
@@ -4419,12 +4578,22 @@ function payloadAccessorForPath(path) {
|
|
|
4419
4578
|
const arrayIndexPos = path.findIndex((segment) => /^\d+$/.test(segment));
|
|
4420
4579
|
if (arrayIndexPos === -1) {
|
|
4421
4580
|
const field = pathToPayloadFieldName(path);
|
|
4422
|
-
return { accessor: `payload.${field}`, field };
|
|
4581
|
+
return { accessor: `payload.${field}`, field, structuredRootPath: null };
|
|
4423
4582
|
}
|
|
4424
4583
|
const objectPath = path.slice(0, arrayIndexPos);
|
|
4425
4584
|
const field = objectPath.length > 0 ? pathToPayloadFieldName(objectPath) : (path[0] ?? "");
|
|
4426
4585
|
const suffix = pathToAccessor(path.slice(arrayIndexPos), { assertNonNull: true });
|
|
4427
|
-
return {
|
|
4586
|
+
return {
|
|
4587
|
+
accessor: `payload.${field}${suffix}`,
|
|
4588
|
+
field,
|
|
4589
|
+
structuredRootPath: objectPath.length > 0 ? objectPath : null,
|
|
4590
|
+
};
|
|
4591
|
+
}
|
|
4592
|
+
/** Reads the value at `path` inside a parsed JSON body, or undefined when any hop is missing. */
|
|
4593
|
+
function valueAtJsonPath(root, path) {
|
|
4594
|
+
return path.reduce((node, segment) => node !== null && typeof node === "object"
|
|
4595
|
+
? node[segment]
|
|
4596
|
+
: undefined, root);
|
|
4428
4597
|
}
|
|
4429
4598
|
/** Derives a valid camelCase identifier from a fixture filename (e.g.
|
|
4430
4599
|
* "10219132.json" -> "fixture10219132", "acme-metrics.config.json" ->
|
|
@@ -5597,12 +5766,29 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
|
|
|
5597
5766
|
/** Every capture's response body from the same run, scanned only for
|
|
5598
5767
|
* {@link CAPACITY_FIELD_NAME_PATTERN}-shaped numeric leaves — the
|
|
5599
5768
|
* evidence source for a discovered "number" key's `capacityMax`. */
|
|
5600
|
-
responseBodiesForCapacitySignal = []) {
|
|
5769
|
+
responseBodiesForCapacitySignal = [], outStructuredKeys = new Map()) {
|
|
5601
5770
|
const merged = [];
|
|
5602
5771
|
const seenPairs = new Set();
|
|
5603
5772
|
const seenValueByKey = new Map();
|
|
5604
5773
|
const distinctValuesByKey = new Map();
|
|
5605
5774
|
const allBodies = [inputBody, ...additionalBodies];
|
|
5775
|
+
// Structure is registered from every body before any scalar is considered, so
|
|
5776
|
+
// a name that is an array/object in one body and a primitive in another gets
|
|
5777
|
+
// the structured schema regardless of which body is visited first.
|
|
5778
|
+
for (const body of allBodies) {
|
|
5779
|
+
if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
5780
|
+
continue;
|
|
5781
|
+
}
|
|
5782
|
+
for (const [key, value] of Object.entries(body)) {
|
|
5783
|
+
if (!isValidJsIdentifier(key))
|
|
5784
|
+
continue;
|
|
5785
|
+
if (value === null || typeof value !== "object")
|
|
5786
|
+
continue;
|
|
5787
|
+
if (Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0)
|
|
5788
|
+
continue;
|
|
5789
|
+
registerStructuredPayloadField(outStructuredKeys, key, value);
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5606
5792
|
for (const body of allBodies) {
|
|
5607
5793
|
if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
5608
5794
|
continue;
|
|
@@ -5613,6 +5799,8 @@ responseBodiesForCapacitySignal = []) {
|
|
|
5613
5799
|
const key = path[0];
|
|
5614
5800
|
if (!isValidJsIdentifier(key))
|
|
5615
5801
|
continue;
|
|
5802
|
+
if (outStructuredKeys.has(key))
|
|
5803
|
+
continue;
|
|
5616
5804
|
if (value === null)
|
|
5617
5805
|
continue;
|
|
5618
5806
|
// Dedupe identical (key, value) pairs only — a repeated occurrence of
|
|
@@ -5781,8 +5969,219 @@ responseBodiesForCapacitySignal = []) {
|
|
|
5781
5969
|
* steps declared any field to correlate against) or when a leaf's value has
|
|
5782
5970
|
* no `key:value` segment matching one of them.
|
|
5783
5971
|
*/
|
|
5784
|
-
|
|
5785
|
-
|
|
5972
|
+
/**
|
|
5973
|
+
* Binds a scalar navigateTo facet at every call site: each object-property
|
|
5974
|
+
* string leaf, at any depth, whose whole value equals a declared facet literal
|
|
5975
|
+
* becomes `${payload.<field>}`. Matching is per whole leaf, so the same
|
|
5976
|
+
* literal flanked by alphanumerics inside an unrelated token is never touched
|
|
5977
|
+
* and no global collision guard can drop the facet. An optional facet falls
|
|
5978
|
+
* back to the captured literal when the caller omits it, never `undefined`.
|
|
5979
|
+
*/
|
|
5980
|
+
function applyScalarFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets) {
|
|
5981
|
+
if (navigateToFacets.length === 0)
|
|
5982
|
+
return template;
|
|
5983
|
+
if (inputBody === null || typeof inputBody !== "object")
|
|
5984
|
+
return template;
|
|
5985
|
+
let result = template;
|
|
5986
|
+
for (const { value, path } of walkAllPrimitiveLeaves(inputBody)) {
|
|
5987
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
5988
|
+
continue;
|
|
5989
|
+
if (/^\d+$/.test(path[path.length - 1] ?? ""))
|
|
5990
|
+
continue;
|
|
5991
|
+
const facet = matchFacetRecurrence(String(value), navigateToFacets, {
|
|
5992
|
+
allowDelimiterSuffix: false,
|
|
5993
|
+
});
|
|
5994
|
+
if (facet === undefined)
|
|
5995
|
+
continue;
|
|
5996
|
+
const literal = JSON.stringify(value);
|
|
5997
|
+
const fallback = facet.optional ? ` ?? ${literal}` : "";
|
|
5998
|
+
const replacement = typeof value === "number" || facet.optional
|
|
5999
|
+
? `\${JSON.stringify(payload.${facet.field}${fallback})}`
|
|
6000
|
+
: `"\${payload.${facet.field}}"`;
|
|
6001
|
+
for (const sep of [":", ": "]) {
|
|
6002
|
+
if (typeof value === "number") {
|
|
6003
|
+
const re = new RegExp(`${sep}${literal}(?![\\w.])`, "g");
|
|
6004
|
+
result = result.replace(re, () => `${sep}${replacement}`);
|
|
6005
|
+
continue;
|
|
6006
|
+
}
|
|
6007
|
+
result = result.split(`${sep}${literal}`).join(`${sep}${replacement}`);
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
return result;
|
|
6011
|
+
}
|
|
6012
|
+
/** Yields every array in `value` (outermost first, at any nesting depth,
|
|
6013
|
+
* including arrays inside array elements) — `{@link
|
|
6014
|
+
* applyArrayFacetSplicePayloadSubstitutions}`'s raw-array search surface. */
|
|
6015
|
+
function* walkArrayLeaves(value, path = []) {
|
|
6016
|
+
if (value === null || typeof value !== "object")
|
|
6017
|
+
return;
|
|
6018
|
+
if (Array.isArray(value)) {
|
|
6019
|
+
yield { value, path };
|
|
6020
|
+
for (const [index, inner] of value.entries()) {
|
|
6021
|
+
yield* walkArrayLeaves(inner, [...path, String(index)]);
|
|
6022
|
+
}
|
|
6023
|
+
return;
|
|
6024
|
+
}
|
|
6025
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
6026
|
+
yield* walkArrayLeaves(inner, [...path, key]);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
/**
|
|
6030
|
+
* REST-body counterpart to {@link spliceFacetsIntoArrayVariable}'s GraphQL
|
|
6031
|
+
* use: a navigateTo facet's literal recurring as one array element (suffixed
|
|
6032
|
+
* by a constant delimiter) inside a REST JSON body gets its element swapped
|
|
6033
|
+
* for `${payload.<field>}` — for EVERY declared facet, required or optional,
|
|
6034
|
+
* at EVERY call site's own array. Matching is structural (see
|
|
6035
|
+
* {@link replaceJsonArrayOccurrences}) and token-boundary guarded per element
|
|
6036
|
+
* ({@link matchArrayElementFacet}), so the splice never depends on the
|
|
6037
|
+
* global, all-or-nothing persona collision guard, on which call site carried
|
|
6038
|
+
* the array first, on element order, or on a facet being optional.
|
|
6039
|
+
*
|
|
6040
|
+
* Every facet-carrying array, whether its facets are required or optional, is
|
|
6041
|
+
* rewritten to the same single `${JSON.stringify([...])}` splice point built
|
|
6042
|
+
* from {@link spliceFacetsIntoArrayVariable}'s per-element conditional
|
|
6043
|
+
* emission, so an absent optional facet drops its element instead of freezing
|
|
6044
|
+
* an `undefined` and no call site renders the array through a different
|
|
6045
|
+
* construction. Non-facet elements stay literal.
|
|
6046
|
+
*/
|
|
6047
|
+
function applyArrayFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets, ownedArrayPathKeys) {
|
|
6048
|
+
if (navigateToFacets.length === 0)
|
|
6049
|
+
return template;
|
|
6050
|
+
if (inputBody === null || typeof inputBody !== "object")
|
|
6051
|
+
return template;
|
|
6052
|
+
let result = template;
|
|
6053
|
+
for (const { value, path } of walkArrayLeaves(inputBody)) {
|
|
6054
|
+
if (value.length === 0)
|
|
6055
|
+
continue;
|
|
6056
|
+
if (!ownedArrayPathKeys.has(arrayPathKey(path)) &&
|
|
6057
|
+
!containsFacetArray(value, navigateToFacets)) {
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
6060
|
+
const expression = renderFacetValueExpression(value, navigateToFacets);
|
|
6061
|
+
result = replaceJsonArrayOccurrences(result, value, `\${JSON.stringify(${expression})}`);
|
|
6062
|
+
}
|
|
6063
|
+
return result;
|
|
6064
|
+
}
|
|
6065
|
+
/** Whether `value` is, or contains at any depth, an array with a
|
|
6066
|
+
* facet-bearing string element. */
|
|
6067
|
+
function containsFacetArray(value, facets) {
|
|
6068
|
+
for (const { value: array } of walkArrayLeaves(value)) {
|
|
6069
|
+
const hasFacet = array.some((element) => typeof element === "string" && matchArrayElementFacet(element, facets));
|
|
6070
|
+
if (hasFacet)
|
|
6071
|
+
return true;
|
|
6072
|
+
}
|
|
6073
|
+
return false;
|
|
6074
|
+
}
|
|
6075
|
+
/** JS expression for `value` with every facet array at any depth spliced, so
|
|
6076
|
+
* one construction covers arrays nested inside array elements. */
|
|
6077
|
+
function renderFacetValueExpression(value, facets) {
|
|
6078
|
+
if (value === null || typeof value !== "object" || !containsFacetArray(value, facets)) {
|
|
6079
|
+
return JSON.stringify(value);
|
|
6080
|
+
}
|
|
6081
|
+
if (Array.isArray(value)) {
|
|
6082
|
+
return (spliceFacetsIntoArrayVariable(value, facets, (element) => renderFacetValueExpression(element, facets)) ?? `[${value.map((element) => renderFacetValueExpression(element, facets)).join(", ")}]`);
|
|
6083
|
+
}
|
|
6084
|
+
const entries = Object.entries(value).map(([key, inner]) => `${JSON.stringify(key)}: ${renderFacetValueExpression(inner, facets)}`);
|
|
6085
|
+
return `{ ${entries.join(", ")} }`;
|
|
6086
|
+
}
|
|
6087
|
+
/** Full index-free path of an array: identifies the array across call sites
|
|
6088
|
+
* whatever index it was captured at, without conflating same-named fields
|
|
6089
|
+
* that live at different paths. */
|
|
6090
|
+
function arrayPathKey(path) {
|
|
6091
|
+
return JSON.stringify(path.filter((segment) => !/^\d+$/.test(segment)));
|
|
6092
|
+
}
|
|
6093
|
+
/** True when `path` addresses anything inside an array in `ownedKeys`, at any
|
|
6094
|
+
* depth (string or object element alike). */
|
|
6095
|
+
function isInsideOwnedArray(path, ownedKeys) {
|
|
6096
|
+
return path.some((segment, index) => /^\d+$/.test(segment) && ownedKeys.has(arrayPathKey(path.slice(0, index))));
|
|
6097
|
+
}
|
|
6098
|
+
/**
|
|
6099
|
+
* Path keys ({@link arrayPathKey}) of every array, in ANY captured body,
|
|
6100
|
+
* that carries at least one facet-bearing element. The facet pass owns those
|
|
6101
|
+
* arrays wholesale, so none of their elements may also be frozen as a
|
|
6102
|
+
* by-index `payload.<field>["N"]` accessor, whichever body was parsed first
|
|
6103
|
+
* and wherever the array sits in it.
|
|
6104
|
+
*/
|
|
6105
|
+
function collectFacetArrayPathKeys(bodies, facets) {
|
|
6106
|
+
const keys = new Set();
|
|
6107
|
+
if (facets.length === 0)
|
|
6108
|
+
return keys;
|
|
6109
|
+
for (const body of bodies) {
|
|
6110
|
+
if (body === null || typeof body !== "object")
|
|
6111
|
+
continue;
|
|
6112
|
+
for (const { value, path } of walkArrayLeaves(body)) {
|
|
6113
|
+
const hasFacet = value.some((element) => typeof element === "string" && matchArrayElementFacet(element, facets) !== undefined);
|
|
6114
|
+
if (hasFacet)
|
|
6115
|
+
keys.add(arrayPathKey(path));
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
return keys;
|
|
6119
|
+
}
|
|
6120
|
+
/**
|
|
6121
|
+
* Rewrites every occurrence of `target` inside `template` by JSON structure
|
|
6122
|
+
* (bracket-balanced, string-aware, whitespace-insensitive) rather than by
|
|
6123
|
+
* exact `JSON.stringify` text, so a pretty-printed or re-spaced body array
|
|
6124
|
+
* is still replaced.
|
|
6125
|
+
*/
|
|
6126
|
+
function replaceJsonArrayOccurrences(template, target, replacement) {
|
|
6127
|
+
const targetKey = JSON.stringify(target);
|
|
6128
|
+
let out = "";
|
|
6129
|
+
let from = 0;
|
|
6130
|
+
while (from < template.length) {
|
|
6131
|
+
const open = template.indexOf("[", from);
|
|
6132
|
+
if (open === -1)
|
|
6133
|
+
break;
|
|
6134
|
+
const close = findMatchingBracket(template, open);
|
|
6135
|
+
const candidate = close === -1 ? undefined : template.slice(open, close + 1);
|
|
6136
|
+
const matches = candidate !== undefined && jsonTextEquals(candidate, targetKey);
|
|
6137
|
+
if (!matches) {
|
|
6138
|
+
out += template.slice(from, open + 1);
|
|
6139
|
+
from = open + 1;
|
|
6140
|
+
continue;
|
|
6141
|
+
}
|
|
6142
|
+
out += template.slice(from, open) + replacement;
|
|
6143
|
+
from = close + 1;
|
|
6144
|
+
}
|
|
6145
|
+
return out + template.slice(from);
|
|
6146
|
+
}
|
|
6147
|
+
function findMatchingBracket(text, open) {
|
|
6148
|
+
let depth = 0;
|
|
6149
|
+
let inString = false;
|
|
6150
|
+
for (let i = open; i < text.length; i++) {
|
|
6151
|
+
const ch = text[i];
|
|
6152
|
+
if (inString) {
|
|
6153
|
+
if (ch === "\\")
|
|
6154
|
+
i++;
|
|
6155
|
+
else if (ch === '"')
|
|
6156
|
+
inString = false;
|
|
6157
|
+
continue;
|
|
6158
|
+
}
|
|
6159
|
+
if (ch === '"')
|
|
6160
|
+
inString = true;
|
|
6161
|
+
else if (ch === "[")
|
|
6162
|
+
depth++;
|
|
6163
|
+
else if (ch === "]" && --depth === 0)
|
|
6164
|
+
return i;
|
|
6165
|
+
}
|
|
6166
|
+
return -1;
|
|
6167
|
+
}
|
|
6168
|
+
function jsonTextEquals(candidate, targetKey) {
|
|
6169
|
+
try {
|
|
6170
|
+
return JSON.stringify(JSON.parse(candidate)) === targetKey;
|
|
6171
|
+
}
|
|
6172
|
+
catch {
|
|
6173
|
+
return false;
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
/** A facet literal recurring inside a packed string under a key that does not
|
|
6177
|
+
* name its payload field. A leaf that IS the bare literal is left to the scalar
|
|
6178
|
+
* pass, which owns the optional fallback for it. */
|
|
6179
|
+
function packedFacetRecurrence(value, facets) {
|
|
6180
|
+
const spliced = spliceFacetRecurrenceIntoScalarVariable(value, facets);
|
|
6181
|
+
return spliced !== null && !spliced.startsWith("payload.") ? spliced : null;
|
|
6182
|
+
}
|
|
6183
|
+
function applyFacetSplicePayloadSubstitutions(template, inputBody, payloadFieldNames, optionalFieldNames, navigateToFacets = []) {
|
|
6184
|
+
if (payloadFieldNames.size === 0 && navigateToFacets.length === 0)
|
|
5786
6185
|
return template;
|
|
5787
6186
|
if (inputBody === undefined ||
|
|
5788
6187
|
inputBody === null ||
|
|
@@ -5793,18 +6192,23 @@ function applyFacetSplicePayloadSubstitutions(template, inputBody, payloadFieldN
|
|
|
5793
6192
|
const fields = [...payloadFieldNames];
|
|
5794
6193
|
let result = template;
|
|
5795
6194
|
for (const { value, path } of walkAllPrimitiveLeaves(inputBody)) {
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
const key = path[0];
|
|
5799
|
-
if (!isValidJsIdentifier(key))
|
|
6195
|
+
const key = path[path.length - 1];
|
|
6196
|
+
if (key === undefined || !isValidJsIdentifier(key))
|
|
5800
6197
|
continue;
|
|
5801
6198
|
if (typeof value !== "string")
|
|
5802
6199
|
continue;
|
|
5803
|
-
const
|
|
6200
|
+
const kv = spliceFacetsIntoStringVariable(value, fields, optionalFieldNames);
|
|
6201
|
+
const spliced = kv ?? packedFacetRecurrence(value, navigateToFacets);
|
|
5804
6202
|
if (spliced === null)
|
|
5805
6203
|
continue;
|
|
5806
6204
|
const target = `"${key}":${JSON.stringify(value)}`;
|
|
5807
|
-
|
|
6205
|
+
// A required-only packed value with nothing JSON-escaped splices in place,
|
|
6206
|
+
// so the accessor sits directly in the string instead of nesting a template.
|
|
6207
|
+
const inline = kv === null &&
|
|
6208
|
+
spliced.startsWith("`") &&
|
|
6209
|
+
!spliced.startsWith("`${") &&
|
|
6210
|
+
JSON.stringify(value).slice(1, -1) === value;
|
|
6211
|
+
const replacement = inline ? `"${key}":"${spliced.slice(1, -1)}"` : `"${key}":"\${${spliced}}"`;
|
|
5808
6212
|
result = result.split(target).join(replacement);
|
|
5809
6213
|
}
|
|
5810
6214
|
return result;
|
|
@@ -6085,8 +6489,12 @@ function parseBodyLeafValues(requestPostData) {
|
|
|
6085
6489
|
* affect (the already-working recurrence-anchored path exists precisely
|
|
6086
6490
|
* because a real facet DOES recur in a later request; nothing here changes
|
|
6087
6491
|
* that — this only supplies values for facets that are not found at all).
|
|
6492
|
+
*
|
|
6493
|
+
* Values that carry a literal of an already-reachable facet are explained by
|
|
6494
|
+
* that facet and never enter the pool, so reachable facets' own transitions
|
|
6495
|
+
* cannot skew the pool size away from the count of facets awaiting a value.
|
|
6088
6496
|
*/
|
|
6089
|
-
function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
6497
|
+
function collectNewlyAppearingRequestValueTransitions(actions, explainedLiterals = []) {
|
|
6090
6498
|
const seenGlobally = new Set();
|
|
6091
6499
|
const transitions = [];
|
|
6092
6500
|
for (let i = 1; i < actions.length; i++) {
|
|
@@ -6097,6 +6505,8 @@ function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
|
6097
6505
|
const record = (value) => {
|
|
6098
6506
|
if (value.length === 0 || seenGlobally.has(value) || seenThisTransition.has(value))
|
|
6099
6507
|
return;
|
|
6508
|
+
if (explainedLiterals.some((literal) => occursAtTokenBoundary(value, literal)))
|
|
6509
|
+
return;
|
|
6100
6510
|
seenThisTransition.add(value);
|
|
6101
6511
|
values.push(value);
|
|
6102
6512
|
};
|
|
@@ -6117,6 +6527,79 @@ function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
|
6117
6527
|
}
|
|
6118
6528
|
return transitions;
|
|
6119
6529
|
}
|
|
6530
|
+
/**
|
|
6531
|
+
* Whether `value` occurs in every request body only at token boundaries (never
|
|
6532
|
+
* flanked by an alphanumeric), so a global rewrite of it cannot mangle a longer
|
|
6533
|
+
* token. Shared by the persona-binding guard and facet identity resolution.
|
|
6534
|
+
*/
|
|
6535
|
+
function bindsWithoutCollisionIn(bodies, value) {
|
|
6536
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
6537
|
+
for (const body of bodies) {
|
|
6538
|
+
let from = 0;
|
|
6539
|
+
while (true) {
|
|
6540
|
+
const at = body.indexOf(value, from);
|
|
6541
|
+
if (at === -1)
|
|
6542
|
+
break;
|
|
6543
|
+
if (isAlnum(body[at - 1]) || isAlnum(body[at + value.length]))
|
|
6544
|
+
return false;
|
|
6545
|
+
from = at + value.length;
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
return true;
|
|
6549
|
+
}
|
|
6550
|
+
/**
|
|
6551
|
+
* Whether `value` occurs in `text` at least once flanked by non-alphanumerics,
|
|
6552
|
+
* so facet reachability agrees with the token-boundary {@link matchFacetRecurrence}
|
|
6553
|
+
* the splice passes use instead of counting a mere substring of a longer token.
|
|
6554
|
+
*/
|
|
6555
|
+
function occursAtTokenBoundary(text, value) {
|
|
6556
|
+
if (value.length === 0)
|
|
6557
|
+
return false;
|
|
6558
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
6559
|
+
let from = 0;
|
|
6560
|
+
while (true) {
|
|
6561
|
+
const at = text.indexOf(value, from);
|
|
6562
|
+
if (at === -1)
|
|
6563
|
+
return false;
|
|
6564
|
+
if (!isAlnum(text[at - 1]) && !isAlnum(text[at + value.length]))
|
|
6565
|
+
return true;
|
|
6566
|
+
from = at + 1;
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
/**
|
|
6570
|
+
* Every literal the flow's declared navigateTo facets take in the captured
|
|
6571
|
+
* traffic: the hash-derived literal plus, for a facet whose hash literal never
|
|
6572
|
+
* recurs, the body/query value {@link correlateUnreachableNavigateToFacets}
|
|
6573
|
+
* pairs it with. Every per-site splice pass matches against this one list, so
|
|
6574
|
+
* a facet is judged to recur identically by all of them instead of each pass
|
|
6575
|
+
* deciding from the hash-derived literal alone.
|
|
6576
|
+
*/
|
|
6577
|
+
function resolveRecurringNavigateToFacets(declared, actions) {
|
|
6578
|
+
const appearsAnywhere = (value) => actions.some(({ capture }) => occursAtTokenBoundary(capture.url, value) ||
|
|
6579
|
+
(capture.requestPostData != null &&
|
|
6580
|
+
occursAtTokenBoundary(capture.requestPostData, value)) ||
|
|
6581
|
+
Object.values(capture.requestHeaders).some((h) => occursAtTokenBoundary(h, value)));
|
|
6582
|
+
const reachable = declared.filter(({ value }) => value.length > 0 && appearsAnywhere(value));
|
|
6583
|
+
const resolvedDeclared = declared.filter(({ value }) => value.length > 0);
|
|
6584
|
+
const unreachable = declared.filter((facet) => !reachable.includes(facet));
|
|
6585
|
+
if (unreachable.length === 0)
|
|
6586
|
+
return resolvedDeclared;
|
|
6587
|
+
const correlated = correlateUnreachableNavigateToFacets(unreachable, actions, reachable.map(({ value }) => value));
|
|
6588
|
+
if (correlated === null)
|
|
6589
|
+
return resolvedDeclared;
|
|
6590
|
+
const bodies = actions
|
|
6591
|
+
.map(({ capture }) => capture.requestPostData)
|
|
6592
|
+
.filter((body) => typeof body === "string" && body.length > 0);
|
|
6593
|
+
const extras = unreachable.flatMap((facet) => {
|
|
6594
|
+
const diffed = correlated.get(facet.field);
|
|
6595
|
+
if (diffed === undefined || diffed.length === 0)
|
|
6596
|
+
return [];
|
|
6597
|
+
if (declared.some(({ value }) => value === diffed))
|
|
6598
|
+
return [];
|
|
6599
|
+
return bindsWithoutCollisionIn(bodies, diffed) ? [{ ...facet, value: diffed }] : [];
|
|
6600
|
+
});
|
|
6601
|
+
return [...resolvedDeclared, ...extras];
|
|
6602
|
+
}
|
|
6120
6603
|
/**
|
|
6121
6604
|
* Resolves each zero-recurrence navigateTo facet (in flow order) to the real
|
|
6122
6605
|
* request value it causally affects, or returns `null` when the correlation
|
|
@@ -6133,14 +6616,27 @@ function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
|
6133
6616
|
* no longer guaranteed to line facets up with the RIGHT transition, so
|
|
6134
6617
|
* nothing is bound — leaving a facet declared-but-unbound is preferable to
|
|
6135
6618
|
* silently wiring it to another facet's (or an unrelated transition's) value.
|
|
6619
|
+
*
|
|
6620
|
+
* When the TOTAL transition count (ambiguous ones included) equals the facet
|
|
6621
|
+
* count, each ambiguous transition still holds its facet's slot, so only that
|
|
6622
|
+
* facet stays unbound while the others bind by position.
|
|
6136
6623
|
*/
|
|
6137
|
-
function correlateUnreachableNavigateToFacets(facets, actions) {
|
|
6138
|
-
const
|
|
6624
|
+
function correlateUnreachableNavigateToFacets(facets, actions, explainedLiterals) {
|
|
6625
|
+
const transitions = collectNewlyAppearingRequestValueTransitions(actions, explainedLiterals);
|
|
6626
|
+
const byField = new Map();
|
|
6627
|
+
if (transitions.length === facets.length) {
|
|
6628
|
+
facets.forEach(({ field }, index) => {
|
|
6629
|
+
const transition = transitions[index];
|
|
6630
|
+
if (transition.length === 1)
|
|
6631
|
+
byField.set(field, transition[0]);
|
|
6632
|
+
});
|
|
6633
|
+
return byField.size > 0 ? byField : null;
|
|
6634
|
+
}
|
|
6635
|
+
const unambiguousValues = transitions
|
|
6139
6636
|
.filter((transition) => transition.length === 1)
|
|
6140
6637
|
.map((transition) => transition[0]);
|
|
6141
6638
|
if (unambiguousValues.length !== facets.length)
|
|
6142
6639
|
return null;
|
|
6143
|
-
const byField = new Map();
|
|
6144
6640
|
facets.forEach(({ field }, index) => {
|
|
6145
6641
|
byField.set(field, unambiguousValues[index]);
|
|
6146
6642
|
});
|
|
@@ -6187,12 +6683,30 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6187
6683
|
// below can correlate a REST body's facet-packed string leaves against it.
|
|
6188
6684
|
const { payloadFieldNames: restPayloadFieldNames, optionalPayloadFieldNames: restOptionalFieldNames, } = computeFlowPayloadFieldNames(flowSteps, vocabulary, process.env);
|
|
6189
6685
|
const payloadAccessorByValue = new Map();
|
|
6686
|
+
const declaredNavigateToFacets = extractNavigateToFacetOrder(flowSteps);
|
|
6687
|
+
const navigateToFacetOrder = resolveRecurringNavigateToFacets(declaredNavigateToFacets, actions);
|
|
6688
|
+
const facetArrayPathKeys = collectFacetArrayPathKeys(actions.map((action) => parseJsonBodyOrNull(action.capture.requestPostData)), navigateToFacetOrder);
|
|
6190
6689
|
if (inputBody !== undefined && inputBody !== null) {
|
|
6191
6690
|
for (const { value, path } of walkStringLeaves(inputBody)) {
|
|
6192
6691
|
if (value.length < MIN_STATE_VALUE_LENGTH)
|
|
6193
6692
|
continue;
|
|
6194
|
-
|
|
6693
|
+
// An element of an array any call site's facet pass owns belongs to the
|
|
6694
|
+
// facet's payloadField (or stays literal), not to a by-index accessor
|
|
6695
|
+
// that would freeze the sibling elements' shape.
|
|
6696
|
+
if (isInsideOwnedArray(path, facetArrayPathKeys))
|
|
6697
|
+
continue;
|
|
6698
|
+
// A leaf recurring as a declared facet literal binds to that facet's
|
|
6699
|
+
// payloadField via the persona pass, not to a by-index accessor.
|
|
6700
|
+
if (matchFacetRecurrence(value, navigateToFacetOrder, { allowDelimiterSuffix: true })) {
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6703
|
+
const { accessor, field: accessorField, structuredRootPath } = payloadAccessorForPath(path);
|
|
6195
6704
|
payloadAccessorByValue.set(value, accessor);
|
|
6705
|
+
// The accessor indexes into this field, so its declared type must be the
|
|
6706
|
+
// structured shape regardless of whether Mechanism B visited the key.
|
|
6707
|
+
if (structuredRootPath !== null) {
|
|
6708
|
+
registerStructuredPayloadField(outStructuredKeys, accessorField, valueAtJsonPath(inputBody, structuredRootPath));
|
|
6709
|
+
}
|
|
6196
6710
|
if (isValidJsIdentifier(accessorField))
|
|
6197
6711
|
outDiscoveredFields.add(accessorField);
|
|
6198
6712
|
// Phase F: register a lowercase variant for UUID-shaped values so case-
|
|
@@ -6282,24 +6796,22 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6282
6796
|
// or when the correlation is ambiguous (see
|
|
6283
6797
|
// `correlateUnreachableNavigateToFacets`), there is nothing safe to splice
|
|
6284
6798
|
// and the field(s) stay unbound rather than risk a wrong binding.
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
const
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
continue;
|
|
6295
|
-
if (!bindsWithoutCollision(diffed))
|
|
6296
|
-
continue;
|
|
6297
|
-
if (payloadAccessorByValue.has(diffed))
|
|
6298
|
-
continue;
|
|
6299
|
-
payloadAccessorByValue.set(diffed, `payload.${field}`);
|
|
6300
|
-
if (isValidJsIdentifier(field))
|
|
6301
|
-
outDiscoveredFields.add(field);
|
|
6799
|
+
// Every facet any splice pass binds comes from the one resolved list, so the
|
|
6800
|
+
// schema declaration and accessor table cannot disagree with the splices.
|
|
6801
|
+
const declaredFacetValues = new Set(declaredNavigateToFacets.map(({ value }) => value));
|
|
6802
|
+
for (const { value, field } of navigateToFacetOrder) {
|
|
6803
|
+
if (!isValidJsIdentifier(field))
|
|
6804
|
+
continue;
|
|
6805
|
+
if (declaredFacetValues.has(value)) {
|
|
6806
|
+
outDiscoveredFields.add(field);
|
|
6807
|
+
continue;
|
|
6302
6808
|
}
|
|
6809
|
+
if (!bindsWithoutCollision(value))
|
|
6810
|
+
continue;
|
|
6811
|
+
if (payloadAccessorByValue.has(value))
|
|
6812
|
+
continue;
|
|
6813
|
+
payloadAccessorByValue.set(value, `payload.${field}`);
|
|
6814
|
+
outDiscoveredFields.add(field);
|
|
6303
6815
|
}
|
|
6304
6816
|
// Job coordinates from the recon entry URL's query string (e.g.
|
|
6305
6817
|
// `?jobSeqNo=...`). Registered the same way as BaseUrl so every verbatim
|
|
@@ -6552,8 +7064,21 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6552
7064
|
// same declared field set, just applied to REST body text instead of a
|
|
6553
7065
|
// variables object literal.
|
|
6554
7066
|
const rawBodyWithFacetSplices = parsedBody !== null
|
|
6555
|
-
? applyFacetSplicePayloadSubstitutions(rawBodyWithFormSubs, parsedBody, restPayloadFieldNames, restOptionalFieldNames)
|
|
7067
|
+
? applyFacetSplicePayloadSubstitutions(rawBodyWithFormSubs, parsedBody, restPayloadFieldNames, restOptionalFieldNames, navigateToFacetOrder)
|
|
6556
7068
|
: rawBodyWithFormSubs;
|
|
7069
|
+
// Optional-facet array-element splice — a navigateTo facet literal
|
|
7070
|
+
// recurring as one element of an array field (not the `key:value`
|
|
7071
|
+
// grammar above) must drop its own element, not freeze an `undefined`,
|
|
7072
|
+
// when its binding is optional and the caller omits it. Runs BEFORE
|
|
7073
|
+
// Mechanism B (below) can swallow the whole array wholesale as an
|
|
7074
|
+
// unparameterized structured value — see
|
|
7075
|
+
// {@link applyArrayFacetSplicePayloadSubstitutions}.
|
|
7076
|
+
const rawBodyWithOptionalArrayFacetSplices = parsedBody !== null
|
|
7077
|
+
? applyArrayFacetSplicePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, navigateToFacetOrder, facetArrayPathKeys)
|
|
7078
|
+
: rawBodyWithFacetSplices;
|
|
7079
|
+
const rawBodyWithScalarFacetSplices = parsedBody !== null
|
|
7080
|
+
? applyScalarFacetSplicePayloadSubstitutions(rawBodyWithOptionalArrayFacetSplices, parsedBody, navigateToFacetOrder)
|
|
7081
|
+
: rawBodyWithOptionalArrayFacetSplices;
|
|
6557
7082
|
// Mechanism B — parameterize whole nested caller structures
|
|
6558
7083
|
// (experienceData/educationData history, opaque eventData) BEFORE value
|
|
6559
7084
|
// substitution reaches inside them: swallowing the entire array/object first
|
|
@@ -6564,7 +7089,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6564
7089
|
// dependent-drill-down join field (e.g. a bulk `{"tokens":[...]}` lookup)
|
|
6565
7090
|
// must stay reachable for state threading, not get frozen as caller data.
|
|
6566
7091
|
const rawBodyWithStructuredSubs = parsedBody !== null
|
|
6567
|
-
? applyStructuredValuePayloadSubstitutions(
|
|
7092
|
+
? applyStructuredValuePayloadSubstitutions(rawBodyWithScalarFacetSplices, parsedBody, outStructuredKeys, deriveStateVarByValue(prior, cap), joinFieldValuesByStep.get(i) ?? new Set(), payloadAccessorByValue, facetArrayPathKeys)
|
|
6568
7093
|
: rawBodyWithFormSubs;
|
|
6569
7094
|
// Whole-value caller coordinates bind here — after structured subs, BEFORE
|
|
6570
7095
|
// state threading — so a composite coordinate (a jobLocation or jobSeqNo
|
|
@@ -6596,7 +7121,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6596
7121
|
? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
|
|
6597
7122
|
: rawBodyWithProducerBoundary;
|
|
6598
7123
|
const bodyAfterStateAndKv = rawBodyWithUrlParams
|
|
6599
|
-
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues, joinFieldValuesByStep.get(i) ?? new Set()), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies)
|
|
7124
|
+
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues, joinFieldValuesByStep.get(i) ?? new Set()), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies, outStructuredKeys)
|
|
6600
7125
|
: "";
|
|
6601
7126
|
// Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
|
|
6602
7127
|
// rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
|
|
@@ -7487,6 +8012,123 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
|
|
|
7487
8012
|
}
|
|
7488
8013
|
return `[${elements.join(", ")}].join("")`;
|
|
7489
8014
|
}
|
|
8015
|
+
/**
|
|
8016
|
+
* Token-boundary match of one array element against the declared navigateTo
|
|
8017
|
+
* facets — the single place elements are matched so the GraphQL and REST
|
|
8018
|
+
* paths (and the REST accessor registration) never disagree on what a
|
|
8019
|
+
* facet-bearing element is.
|
|
8020
|
+
*/
|
|
8021
|
+
function matchArrayElementFacet(element, facets) {
|
|
8022
|
+
return matchFacetRecurrence(element, facets, { allowDelimiterSuffix: true });
|
|
8023
|
+
}
|
|
8024
|
+
/**
|
|
8025
|
+
* The one facet-recurrence predicate every grammar (array element, scalar
|
|
8026
|
+
* leaf) consults, so a declared facet literal is judged to recur identically
|
|
8027
|
+
* everywhere. A number leaf is compared by its string form, so a numeric body
|
|
8028
|
+
* value matches the string literal a navigateTo hash yields. With
|
|
8029
|
+
* `allowDelimiterSuffix`, a literal followed by a non-alphanumeric delimiter
|
|
8030
|
+
* suffix also matches. The longest literal wins.
|
|
8031
|
+
*/
|
|
8032
|
+
function matchFacetRecurrence(text, facets, options) {
|
|
8033
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
8034
|
+
return facets.reduce((best, facet) => {
|
|
8035
|
+
const literal = facet.value;
|
|
8036
|
+
const matches = text === literal ||
|
|
8037
|
+
(options.allowDelimiterSuffix &&
|
|
8038
|
+
literal.length > 0 &&
|
|
8039
|
+
text.startsWith(literal) &&
|
|
8040
|
+
!isAlnum(text[literal.length]));
|
|
8041
|
+
if (!matches)
|
|
8042
|
+
return best;
|
|
8043
|
+
return best === undefined || literal.length > best.value.length ? facet : best;
|
|
8044
|
+
}, undefined);
|
|
8045
|
+
}
|
|
8046
|
+
/**
|
|
8047
|
+
* Binds a top-level GraphQL variable whose scalar value (or, for a string, any
|
|
8048
|
+
* `key:value` packed segment's value) recurs as a declared navigateTo facet
|
|
8049
|
+
* literal, via the shared {@link matchFacetRecurrence} predicate so GraphQL
|
|
8050
|
+
* judges recurrence exactly as the array and REST scalar passes do.
|
|
8051
|
+
*/
|
|
8052
|
+
function spliceFacetRecurrenceIntoScalarVariable(value, facets) {
|
|
8053
|
+
if (facets.length === 0)
|
|
8054
|
+
return null;
|
|
8055
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
8056
|
+
const facet = matchFacetRecurrence(String(value), facets, { allowDelimiterSuffix: false });
|
|
8057
|
+
if (facet !== undefined)
|
|
8058
|
+
return `payload.${facet.field}`;
|
|
8059
|
+
}
|
|
8060
|
+
if (typeof value !== "string")
|
|
8061
|
+
return null;
|
|
8062
|
+
const segments = value.split(/([|,;])/);
|
|
8063
|
+
let bound = false;
|
|
8064
|
+
let anyOptional = false;
|
|
8065
|
+
const units = [];
|
|
8066
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
8067
|
+
const segment = segments[index];
|
|
8068
|
+
const trailingDelimiter = segments[index + 1] ?? "";
|
|
8069
|
+
const colonIndex = segment.indexOf(":");
|
|
8070
|
+
const facet = colonIndex < 0
|
|
8071
|
+
? undefined
|
|
8072
|
+
: matchFacetRecurrence(segment.slice(colonIndex + 1), facets, {
|
|
8073
|
+
allowDelimiterSuffix: false,
|
|
8074
|
+
});
|
|
8075
|
+
if (facet === undefined) {
|
|
8076
|
+
units.push(`\`${escapeForTemplateLiteral(segment + trailingDelimiter)}\``);
|
|
8077
|
+
continue;
|
|
8078
|
+
}
|
|
8079
|
+
bound = true;
|
|
8080
|
+
const unit = `\`${escapeForTemplateLiteral(segment.slice(0, colonIndex + 1))}\${payload.${facet.field}}${escapeForTemplateLiteral(trailingDelimiter)}\``;
|
|
8081
|
+
// An absent optional facet drops its own segment (and delimiter) rather
|
|
8082
|
+
// than rendering the string `undefined` into the packed value.
|
|
8083
|
+
if (facet.optional) {
|
|
8084
|
+
anyOptional = true;
|
|
8085
|
+
units.push(`...(payload.${facet.field} ? [${unit}] : [])`);
|
|
8086
|
+
continue;
|
|
8087
|
+
}
|
|
8088
|
+
units.push(unit);
|
|
8089
|
+
}
|
|
8090
|
+
if (!bound)
|
|
8091
|
+
return null;
|
|
8092
|
+
if (anyOptional)
|
|
8093
|
+
return `[${units.join(", ")}].join("")`;
|
|
8094
|
+
return `\`${units.map((unit) => unit.slice(1, -1)).join("")}\``;
|
|
8095
|
+
}
|
|
8096
|
+
/**
|
|
8097
|
+
* Splices a captured array-valued GraphQL variable's elements against
|
|
8098
|
+
* navigateTo facet bindings instead of the `key:value` segment grammar
|
|
8099
|
+
* {@link spliceFacetsIntoStringVariable} matches — an array element like
|
|
8100
|
+
* `widget-x;filterId=urlFriendlyId` carries a bare literal plus a constant
|
|
8101
|
+
* trailing delimiter, with no `key:` prefix to key off of. Matching is
|
|
8102
|
+
* token-boundary guarded the same way {@link bindsWithoutCollision} (recon-
|
|
8103
|
+
* generate.ts ~7223) guards persona bindings elsewhere in this file, so a
|
|
8104
|
+
* facet literal that's merely a substring of an unrelated element never
|
|
8105
|
+
* misfires. A matched binding whose `optional` flag is set is emitted as a
|
|
8106
|
+
* conditionally-included array element (`...(payload.<field> ? [...] : [])`)
|
|
8107
|
+
* instead of inline, the same optional-splice convention
|
|
8108
|
+
* {@link spliceFacetsIntoStringVariable} uses, so an absent optional facet
|
|
8109
|
+
* drops its element rather than emitting `undefined`.
|
|
8110
|
+
*/
|
|
8111
|
+
function spliceFacetsIntoArrayVariable(value, facets, renderElement = (element) => JSON.stringify(element)) {
|
|
8112
|
+
if (!Array.isArray(value))
|
|
8113
|
+
return null;
|
|
8114
|
+
let matchCount = 0;
|
|
8115
|
+
const elements = value.map((element) => {
|
|
8116
|
+
if (typeof element !== "string")
|
|
8117
|
+
return renderElement(element);
|
|
8118
|
+
const matched = matchArrayElementFacet(element, facets);
|
|
8119
|
+
if (!matched)
|
|
8120
|
+
return renderElement(element);
|
|
8121
|
+
matchCount++;
|
|
8122
|
+
const suffix = element.slice(matched.value.length);
|
|
8123
|
+
const elementLiteral = `\`\${payload.${matched.field}}${escapeForTemplateLiteral(suffix)}\``;
|
|
8124
|
+
return matched.optional
|
|
8125
|
+
? `...(payload.${matched.field} ? [${elementLiteral}] : [])`
|
|
8126
|
+
: elementLiteral;
|
|
8127
|
+
});
|
|
8128
|
+
if (matchCount === 0)
|
|
8129
|
+
return null;
|
|
8130
|
+
return `[${elements.join(", ")}]`;
|
|
8131
|
+
}
|
|
7490
8132
|
/**
|
|
7491
8133
|
* Renders the variables literal for the primary-operation getGql() call —
|
|
7492
8134
|
* each key from the selected capture's own recorded variables is bound to
|
|
@@ -7498,23 +8140,35 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
|
|
|
7498
8140
|
* other value is emitted verbatim via JSON.stringify. `optionalFieldNames`
|
|
7499
8141
|
* marks which of `payloadFieldNames` a facet-string splice should treat as
|
|
7500
8142
|
* optional (see {@link spliceFacetsIntoStringVariable}); it has no effect on
|
|
7501
|
-
* top-level key correlation.
|
|
8143
|
+
* top-level key correlation. When a value is an array (not itself a string
|
|
8144
|
+
* key correlation candidate), its elements are matched against
|
|
8145
|
+
* `navigateToFacets` instead — see {@link spliceFacetsIntoArrayVariable} for
|
|
8146
|
+
* why that's a distinct grammar from the `key:value` one above.
|
|
7502
8147
|
*/
|
|
7503
|
-
function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames = new Set()) {
|
|
8148
|
+
function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames = new Set(), navigateToFacets = []) {
|
|
7504
8149
|
if (variables === null || typeof variables !== "object" || Array.isArray(variables))
|
|
7505
8150
|
return "{}";
|
|
7506
8151
|
const fields = payloadFieldNames ? [...payloadFieldNames] : [];
|
|
7507
|
-
const
|
|
7508
|
-
const
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
8152
|
+
const renderValue = (value) => {
|
|
8153
|
+
const spliced = spliceFacetsIntoStringVariable(value, fields, optionalFieldNames) ??
|
|
8154
|
+
spliceFacetsIntoArrayVariable(value, navigateToFacets, renderValue) ??
|
|
8155
|
+
spliceFacetRecurrenceIntoScalarVariable(value, navigateToFacets);
|
|
8156
|
+
if (spliced !== null)
|
|
8157
|
+
return spliced;
|
|
8158
|
+
if (Array.isArray(value))
|
|
8159
|
+
return `[${value.map(renderValue).join(", ")}]`;
|
|
8160
|
+
if (value !== null && typeof value === "object")
|
|
8161
|
+
return renderObject(value);
|
|
8162
|
+
return JSON.stringify(value);
|
|
8163
|
+
};
|
|
8164
|
+
const renderObject = (obj) => {
|
|
8165
|
+
const entries = Object.entries(obj).map(([key, value]) => {
|
|
8166
|
+
const matchedField = fields.find((field) => field.toLowerCase() === key.toLowerCase());
|
|
8167
|
+
return `${isValidJsIdentifier(key) ? key : JSON.stringify(key)}: ${matchedField ? `payload.${matchedField}` : renderValue(value)}`;
|
|
8168
|
+
});
|
|
8169
|
+
return entries.length > 0 ? `{ ${entries.join(", ")} }` : "{}";
|
|
8170
|
+
};
|
|
8171
|
+
return renderObject(variables);
|
|
7518
8172
|
}
|
|
7519
8173
|
/** A count-style page-size key (e.g. `count`, `limit`, `first`) paired with a
|
|
7520
8174
|
* skip-style offset key (e.g. `skip`, `offset`) in the same object is the
|
|
@@ -7945,7 +8599,7 @@ function isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, sibl
|
|
|
7945
8599
|
* iteration's request, exactly the defect described in
|
|
7946
8600
|
* docs/recon-generate-nested-fold-flatmaps-away-the-parent-so-drill-params-freeze.md
|
|
7947
8601
|
* (a `packageCode`/`groupId`/`sailDate` triple that provably varies per
|
|
7948
|
-
*
|
|
8602
|
+
* voyage, silently frozen because no threaded field explained it). Path
|
|
7949
8603
|
* segments are deliberately not checked: {@link endpointKey} requires an
|
|
7950
8604
|
* identical pathname to group two captures at all, so no path segment can
|
|
7951
8605
|
* ever be observed to vary within a matched group.
|
|
@@ -10519,10 +11173,68 @@ function buildContractChecklist(opts) {
|
|
|
10519
11173
|
outOfTreeChecklistLine,
|
|
10520
11174
|
].filter((line) => line !== "");
|
|
10521
11175
|
}
|
|
11176
|
+
const PAYLOAD_ACCESSOR_CHAIN = /\bpayload\.([A-Za-z_$][A-Za-z0-9_$]*)((?:!|\??\.[A-Za-z_$][A-Za-z0-9_$]*(?![A-Za-z0-9_$(])|\[(?:"[^"\]]*"|\d+)\])*)/g;
|
|
11177
|
+
const PAYLOAD_CHAIN_SEGMENT = /\??\.([A-Za-z_$][A-Za-z0-9_$]*)|\[(?:"[^"\]]*"|\d+)\]/g;
|
|
11178
|
+
function newAccessorShape() {
|
|
11179
|
+
return { isArray: false, element: null, keys: new Map() };
|
|
11180
|
+
}
|
|
11181
|
+
function renderAccessorShape(shape) {
|
|
11182
|
+
if (shape.isArray) {
|
|
11183
|
+
return `z.array(${shape.element ? renderAccessorShape(shape.element) : "z.string()"})`;
|
|
11184
|
+
}
|
|
11185
|
+
if (shape.keys.size === 0)
|
|
11186
|
+
return "z.string()";
|
|
11187
|
+
const entries = [...shape.keys.entries()]
|
|
11188
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
11189
|
+
.map(([key, child]) => `${isValidJsIdentifier(key) ? key : JSON.stringify(key)}: ${renderAccessorShape(child)}`);
|
|
11190
|
+
return `z.object({ ${entries.join(", ")} })`;
|
|
11191
|
+
}
|
|
11192
|
+
/**
|
|
11193
|
+
* Derives each `payload.<field>` Zod type from the full accessor chain the
|
|
11194
|
+
* emitted body text uses (`payload.f["0"]!.x` => array of objects with key
|
|
11195
|
+
* `x`), so the schema can never reject a property access the body performs.
|
|
11196
|
+
*/
|
|
11197
|
+
function deriveBodyAccessorShapes(body) {
|
|
11198
|
+
const roots = new Map();
|
|
11199
|
+
for (const m of body.matchAll(PAYLOAD_ACCESSOR_CHAIN)) {
|
|
11200
|
+
const root = roots.get(m[1]) ?? newAccessorShape();
|
|
11201
|
+
roots.set(m[1], root);
|
|
11202
|
+
let node = root;
|
|
11203
|
+
for (const seg of m[2].matchAll(PAYLOAD_CHAIN_SEGMENT)) {
|
|
11204
|
+
if (seg[1] === undefined) {
|
|
11205
|
+
node.isArray = true;
|
|
11206
|
+
node.element ??= newAccessorShape();
|
|
11207
|
+
node = node.element;
|
|
11208
|
+
continue;
|
|
11209
|
+
}
|
|
11210
|
+
const child = node.keys.get(seg[1]) ?? newAccessorShape();
|
|
11211
|
+
node.keys.set(seg[1], child);
|
|
11212
|
+
node = child;
|
|
11213
|
+
}
|
|
11214
|
+
}
|
|
11215
|
+
return roots;
|
|
11216
|
+
}
|
|
11217
|
+
function isStructuredAccessorShape(shape) {
|
|
11218
|
+
return shape.isArray || shape.keys.size > 0;
|
|
11219
|
+
}
|
|
11220
|
+
/**
|
|
11221
|
+
* Whether a declared Zod expression already admits every access the shape
|
|
11222
|
+
* demands (array-ness and each dereferenced key), so a richer structured
|
|
11223
|
+
* declaration is kept instead of being narrowed to the accessor-derived one.
|
|
11224
|
+
*/
|
|
11225
|
+
function declaredSchemaCoversAccessorShape(declared, shape) {
|
|
11226
|
+
if (shape.isArray && !/\bz\.array\(/.test(declared))
|
|
11227
|
+
return false;
|
|
11228
|
+
if (!shape.isArray && shape.keys.size > 0 && !/\bz\.(object|record)\(/.test(declared)) {
|
|
11229
|
+
return false;
|
|
11230
|
+
}
|
|
11231
|
+
const childShapes = [...(shape.element ? [shape.element] : []), ...shape.keys.values()];
|
|
11232
|
+
return ([...shape.keys.keys()].every((key) => new RegExp(`(?:^|[\\s{,])"?${key.replace(/[$]/g, "\\$&")}"?\\s*:`).test(declared)) && childShapes.every((child) => declaredSchemaCoversAccessorShape(declared, child)));
|
|
11233
|
+
}
|
|
10522
11234
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
10523
11235
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
10524
11236
|
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;
|
|
11237
|
+
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
11238
|
const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
|
|
10527
11239
|
/** Single source of truth for every GraphQL-emission decision below
|
|
10528
11240
|
* (clientImport, gqlCacheBlock, queryConst, and both executeHttpBody
|
|
@@ -10680,7 +11392,10 @@ function emitContractTs(opts) {
|
|
|
10680
11392
|
// override semantics a chain of `.extend()` calls used to have (each
|
|
10681
11393
|
// subsequent `.extend` replaced an earlier field of the same name).
|
|
10682
11394
|
const extendFields = new Map();
|
|
11395
|
+
const structuredFieldNames = new Set();
|
|
10683
11396
|
const addExtendField = (name, line) => {
|
|
11397
|
+
if (structuredFieldNames.has(name))
|
|
11398
|
+
return;
|
|
10684
11399
|
extendFields.set(name, line);
|
|
10685
11400
|
};
|
|
10686
11401
|
// A detected bounded-paging signal means buildPaginatedFetchLoopExecuteHttpBody
|
|
@@ -10838,7 +11553,8 @@ function emitContractTs(opts) {
|
|
|
10838
11553
|
continue;
|
|
10839
11554
|
const key = isValidJsIdentifier(name) ? name : JSON.stringify(name);
|
|
10840
11555
|
const value = payloadNeedsMultipart ? `multipartJsonObject(${schema})` : schema;
|
|
10841
|
-
|
|
11556
|
+
extendFields.set(name, ` ${key}: ${value},`);
|
|
11557
|
+
structuredFieldNames.add(name);
|
|
10842
11558
|
}
|
|
10843
11559
|
// Closing-the-loop safety net: every discovered-field source above tracks
|
|
10844
11560
|
// its own registration as it splices a `payload.<field>` accessor into the
|
|
@@ -10857,14 +11573,34 @@ function emitContractTs(opts) {
|
|
|
10857
11573
|
// its structural root regardless of which upstream pass forgot to record a
|
|
10858
11574
|
// field, instead of adding a fifth registration site that could itself be
|
|
10859
11575
|
// forgotten by a future pass.
|
|
11576
|
+
const accessorStructuredFields = new Set();
|
|
10860
11577
|
if (multiStepBody) {
|
|
10861
|
-
const bodyReferencedFields =
|
|
10862
|
-
for (const name of [...bodyReferencedFields].sort()) {
|
|
10863
|
-
if (extendFields.has(name))
|
|
10864
|
-
continue;
|
|
11578
|
+
const bodyReferencedFields = deriveBodyAccessorShapes(multiStepBody);
|
|
11579
|
+
for (const name of [...bodyReferencedFields.keys()].sort()) {
|
|
10865
11580
|
if (isReservedByApplicantContactSchema(name))
|
|
10866
11581
|
continue;
|
|
10867
|
-
|
|
11582
|
+
const shape = bodyReferencedFields.get(name);
|
|
11583
|
+
const structured = isStructuredAccessorShape(shape);
|
|
11584
|
+
// The body text is the ground truth for a field's shape: a scalar
|
|
11585
|
+
// registration from any source must not outlive an accessor that indexes
|
|
11586
|
+
// or dereferences the field, and a structured declaration is kept only
|
|
11587
|
+
// when it already admits every access the body performs.
|
|
11588
|
+
if (structured) {
|
|
11589
|
+
const declared = extendFields.get(name);
|
|
11590
|
+
if (structuredFieldNames.has(name) &&
|
|
11591
|
+
declared !== undefined &&
|
|
11592
|
+
declaredSchemaCoversAccessorShape(declared, shape)) {
|
|
11593
|
+
accessorStructuredFields.add(name);
|
|
11594
|
+
continue;
|
|
11595
|
+
}
|
|
11596
|
+
accessorStructuredFields.add(name);
|
|
11597
|
+
structuredFieldNames.delete(name);
|
|
11598
|
+
}
|
|
11599
|
+
else if (extendFields.has(name)) {
|
|
11600
|
+
continue;
|
|
11601
|
+
}
|
|
11602
|
+
const zod = renderAccessorShape(shape);
|
|
11603
|
+
addExtendField(name, ` ${name}: ${zod === "z.string()" || !payloadNeedsMultipart ? zod : `multipartJsonObject(${zod})`},`);
|
|
10868
11604
|
}
|
|
10869
11605
|
}
|
|
10870
11606
|
// The structural walk over the captured request body that used to BE the
|
|
@@ -10912,6 +11648,8 @@ function emitContractTs(opts) {
|
|
|
10912
11648
|
// capture this run and is a no-op, not an error: there is no schema line
|
|
10913
11649
|
// for it to attach to.
|
|
10914
11650
|
for (const [fieldName, constraint] of Object.entries(valueConstraints)) {
|
|
11651
|
+
if (accessorStructuredFields.has(fieldName))
|
|
11652
|
+
continue;
|
|
10915
11653
|
const line = extendFields.get(fieldName);
|
|
10916
11654
|
if (line === null || line === undefined)
|
|
10917
11655
|
continue;
|
|
@@ -11032,10 +11770,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
11032
11770
|
// the same captured request-variables object the signal was itself
|
|
11033
11771
|
// detected from (see paginationOperationIdentity above) — the default
|
|
11034
11772
|
// `{ q: payload.query }` REST body has no skip/count container to advance.
|
|
11773
|
+
const navigateToFacets = resolveRecurringNavigateToFacets(extractNavigateToFacetOrder(contractFlowSteps), actionSteps);
|
|
11035
11774
|
const gqlVariablesExpr = gqlOperationName
|
|
11036
|
-
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
11775
|
+
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
|
|
11037
11776
|
: paginationSignal
|
|
11038
|
-
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
11777
|
+
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
|
|
11039
11778
|
: "{ q: payload.query }";
|
|
11040
11779
|
/** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
|
|
11041
11780
|
* — that fold every resolved plan's drill-down data onto `dataVarName`'s
|
|
@@ -12906,6 +13645,7 @@ async function main() {
|
|
|
12906
13645
|
isSubmissionFlow,
|
|
12907
13646
|
inputBody,
|
|
12908
13647
|
hasMultipartStep,
|
|
13648
|
+
flowSteps,
|
|
12909
13649
|
actionSteps,
|
|
12910
13650
|
foldReturnSpec,
|
|
12911
13651
|
discoveredFormFields,
|