@enricai/barnacle 1.12.77 → 1.12.79
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/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +2 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/actionable-visibility-expr.d.ts +12 -0
- package/dist/scraper/actionable-visibility-expr.d.ts.map +1 -0
- package/dist/scraper/actionable-visibility-expr.js +22 -0
- package/dist/scraper/actionable-visibility-expr.js.map +1 -0
- package/dist/scraper/browser-click-expr.d.ts +1 -1
- package/dist/scraper/browser-click-expr.d.ts.map +1 -1
- package/dist/scraper/browser-click-expr.js +2 -6
- package/dist/scraper/browser-click-expr.js.map +1 -1
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
- package/dist/scraper/deep-locator-candidates.js +35 -11
- package/dist/scraper/deep-locator-candidates.js.map +1 -1
- package/dist/scraper/deep-locator-scan.d.ts +12 -0
- package/dist/scraper/deep-locator-scan.d.ts.map +1 -1
- package/dist/scraper/deep-locator-scan.js +42 -13
- package/dist/scraper/deep-locator-scan.js.map +1 -1
- package/dist/scraper/errors.d.ts +10 -0
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +19 -1
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +47 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +310 -61
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +3 -3
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +6 -14
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/pool.d.ts.map +1 -1
- package/dist/scraper/pool.js +8 -1
- package/dist/scraper/pool.js.map +1 -1
- package/dist/scraper/retry.d.ts +1 -1
- package/dist/scraper/retry.d.ts.map +1 -1
- package/dist/scraper/retry.js +5 -1
- package/dist/scraper/retry.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +29 -17
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-shared.d.ts +5 -0
- package/dist/scraper/session-shared.d.ts.map +1 -1
- package/dist/scraper/session-shared.js.map +1 -1
- package/dist/scraper/session-teardown.d.ts +30 -1
- package/dist/scraper/session-teardown.d.ts.map +1 -1
- package/dist/scraper/session-teardown.js +48 -3
- package/dist/scraper/session-teardown.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +2 -2
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +5 -21
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scraper/target-resolution-diagnostic.d.ts.map +1 -1
- package/dist/scraper/target-resolution-diagnostic.js +6 -11
- package/dist/scraper/target-resolution-diagnostic.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 +25 -6
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +117 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +801 -166
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -62,13 +62,18 @@ 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;
|
|
69
71
|
exports.spliceFacetsIntoArrayVariable = spliceFacetsIntoArrayVariable;
|
|
72
|
+
exports.renderGqlVariablesExpr = renderGqlVariablesExpr;
|
|
70
73
|
exports.findAllObjectArrayFields = findAllObjectArrayFields;
|
|
71
74
|
exports.findAllObjectArrayFieldsOrWholeObject = findAllObjectArrayFieldsOrWholeObject;
|
|
75
|
+
exports.collectRequestBodyValuesByKey = collectRequestBodyValuesByKey;
|
|
76
|
+
exports.findFrozenVaryingDrillParams = findFrozenVaryingDrillParams;
|
|
72
77
|
exports.applyDrillParamBindings = applyDrillParamBindings;
|
|
73
78
|
exports.collectResponseLeafValues = collectResponseLeafValues;
|
|
74
79
|
exports.getScanPrimaryCandidateGroupsCallCountForTest = getScanPrimaryCandidateGroupsCallCountForTest;
|
|
@@ -76,6 +81,7 @@ exports.resetScanPrimaryCandidateGroupsCallCountForTest = resetScanPrimaryCandid
|
|
|
76
81
|
exports.detectDrillDownFoldPlan = detectDrillDownFoldPlan;
|
|
77
82
|
exports.parseFoldReturnSpec = parseFoldReturnSpec;
|
|
78
83
|
exports.parseFallbackGateSpec = parseFallbackGateSpec;
|
|
84
|
+
exports.readComparableBodyLeaf = readComparableBodyLeaf;
|
|
79
85
|
exports.collectRequestValuesIncludingHeaders = collectRequestValuesIncludingHeaders;
|
|
80
86
|
exports.getFoldPlanResolutionCallCountForTests = getFoldPlanResolutionCallCountForTests;
|
|
81
87
|
exports.resetFoldPlanResolutionCallCountForTests = resetFoldPlanResolutionCallCountForTests;
|
|
@@ -382,14 +388,53 @@ function extractNavigateToHashFragmentValue(url, previousHash) {
|
|
|
382
388
|
if (hashIndex === -1)
|
|
383
389
|
return null;
|
|
384
390
|
const hash = url.slice(hashIndex + 1);
|
|
385
|
-
if (previousHash
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
391
|
+
if (previousHash) {
|
|
392
|
+
// A growing hash can add more than one token per step (a "label/value"
|
|
393
|
+
// pair, or a "key=value" pair), at either end, joined by any hash
|
|
394
|
+
// separator. The facet's actual VALUE is the last token that is new
|
|
395
|
+
// relative to the previous hash, with any "key=" prefix removed.
|
|
396
|
+
const previousTokens = new Set(splitHashTokens(previousHash));
|
|
397
|
+
const added = splitHashTokens(hash).filter((token) => !previousTokens.has(token));
|
|
398
|
+
const delta = added[added.length - 1];
|
|
399
|
+
if (delta !== undefined)
|
|
400
|
+
return stripHashKeyPrefix(delta);
|
|
401
|
+
}
|
|
402
|
+
const tokens = splitHashTokens(hash);
|
|
403
|
+
return tokens.length > 0 ? stripHashKeyPrefix(tokens[tokens.length - 1]) : null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Single walk over navigateTo+payloadField steps shared by
|
|
407
|
+
* {@link harvestPersonaBindings} and {@link extractNavigateToFacetOrder}, so
|
|
408
|
+
* both derive identical literals. The delta baseline is the most recent
|
|
409
|
+
* hash-bearing navigateTo; a hashless reset navigation must not erase it, or
|
|
410
|
+
* the next cumulative hash would be taken whole and never recur in a request.
|
|
411
|
+
*/
|
|
412
|
+
function walkNavigateToFacets(flowSteps) {
|
|
413
|
+
const facets = [];
|
|
414
|
+
let baselineHash;
|
|
415
|
+
for (const step of flowSteps) {
|
|
416
|
+
if (typeof step === "string" || step.navigateTo === undefined)
|
|
417
|
+
continue;
|
|
418
|
+
const hashIndex = step.navigateTo.indexOf("#");
|
|
419
|
+
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
420
|
+
if (step.payloadField) {
|
|
421
|
+
facets.push({
|
|
422
|
+
value: extractNavigateToHashFragmentValue(step.navigateTo, baselineHash),
|
|
423
|
+
field: step.payloadField,
|
|
424
|
+
optional: step.optional === true,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (currentHash !== undefined)
|
|
428
|
+
baselineHash = currentHash;
|
|
390
429
|
}
|
|
391
|
-
|
|
392
|
-
|
|
430
|
+
return facets;
|
|
431
|
+
}
|
|
432
|
+
function splitHashTokens(hash) {
|
|
433
|
+
return hash.split(/[,/;&]/).filter((token) => token.length > 0);
|
|
434
|
+
}
|
|
435
|
+
function stripHashKeyPrefix(token) {
|
|
436
|
+
const stripped = token.slice(token.indexOf("=") + 1);
|
|
437
|
+
return stripped.length > 0 ? stripped : token;
|
|
393
438
|
}
|
|
394
439
|
/**
|
|
395
440
|
* Derives a payload field name from the field LABEL in a fill/enter/type
|
|
@@ -437,7 +482,11 @@ function deriveFillLabelField(instruction) {
|
|
|
437
482
|
function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
438
483
|
const bindings = new Map();
|
|
439
484
|
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
440
|
-
|
|
485
|
+
for (const facet of walkNavigateToFacets(flowSteps)) {
|
|
486
|
+
if (facet.value !== null && !bindings.has(facet.value)) {
|
|
487
|
+
bindings.set(facet.value, `payload.${facet.field}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
441
490
|
for (const step of flowSteps) {
|
|
442
491
|
const isObj = typeof step !== "string";
|
|
443
492
|
const instruction = isObj ? step.step : step;
|
|
@@ -448,21 +497,8 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
448
497
|
// `.../#/jobs/category-widgets`), so the value to correlate against
|
|
449
498
|
// downstream header/body templates is the URL's own hash-derived
|
|
450
499
|
// fragment, not anything extractStepPersonaValue could find in `step.step`.
|
|
451
|
-
if (isObj && step.navigateTo !== undefined)
|
|
452
|
-
const hashIndex = step.navigateTo.indexOf("#");
|
|
453
|
-
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
454
|
-
if (!step.payloadField) {
|
|
455
|
-
previousNavigateToHash = currentHash;
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
const value = extractNavigateToHashFragmentValue(step.navigateTo, previousNavigateToHash);
|
|
459
|
-
previousNavigateToHash = currentHash;
|
|
460
|
-
if (value === null)
|
|
461
|
-
continue;
|
|
462
|
-
if (!bindings.has(value))
|
|
463
|
-
bindings.set(value, `payload.${step.payloadField}`);
|
|
500
|
+
if (isObj && step.navigateTo !== undefined)
|
|
464
501
|
continue;
|
|
465
|
-
}
|
|
466
502
|
const vocabField = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
467
503
|
// Vocabulary wins outright. Only on a miss do we fall back to deriving the
|
|
468
504
|
// field from the instruction's own label — and never when the author opted
|
|
@@ -492,26 +528,18 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
492
528
|
* which DOES see the captures, re-derive the same (value, field) pairs and
|
|
493
529
|
* try the causally-adjacent-action fallback (see `emitMultiStepExecuteHttp`)
|
|
494
530
|
* for whichever ones the recurrence-anchored path left unbound.
|
|
531
|
+
*
|
|
532
|
+
* A step whose URL carries no hash literal still holds its slot (empty
|
|
533
|
+
* `value`) so positional correlation against captured transitions stays
|
|
534
|
+
* aligned with the declared facets; {@link resolveRecurringNavigateToFacets}
|
|
535
|
+
* drops the empty literal itself and keeps only a correlated value.
|
|
495
536
|
*/
|
|
496
537
|
function extractNavigateToFacetOrder(flowSteps) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const hashIndex = step.navigateTo.indexOf("#");
|
|
503
|
-
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
504
|
-
if (!step.payloadField) {
|
|
505
|
-
previousNavigateToHash = currentHash;
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
const value = extractNavigateToHashFragmentValue(step.navigateTo, previousNavigateToHash);
|
|
509
|
-
previousNavigateToHash = currentHash;
|
|
510
|
-
if (value === null || value.length === 0)
|
|
511
|
-
continue;
|
|
512
|
-
order.push({ value, field: step.payloadField, optional: step.optional === true });
|
|
513
|
-
}
|
|
514
|
-
return order;
|
|
538
|
+
return walkNavigateToFacets(flowSteps).map((facet) => ({
|
|
539
|
+
value: facet.value ?? "",
|
|
540
|
+
field: facet.field,
|
|
541
|
+
optional: facet.optional,
|
|
542
|
+
}));
|
|
515
543
|
}
|
|
516
544
|
/**
|
|
517
545
|
* Detects a flow step whose quoted VALUE is the space-joined concatenation of
|
|
@@ -2824,6 +2852,17 @@ function jsonBodyLeafValues(requestPostData) {
|
|
|
2824
2852
|
}
|
|
2825
2853
|
return values;
|
|
2826
2854
|
}
|
|
2855
|
+
/** Parses a captured request body as JSON; null for an absent or non-JSON body (multipart raw bytes). */
|
|
2856
|
+
function parseJsonBodyOrNull(requestPostData) {
|
|
2857
|
+
if (typeof requestPostData !== "string" || requestPostData.length === 0)
|
|
2858
|
+
return null;
|
|
2859
|
+
try {
|
|
2860
|
+
return JSON.parse(requestPostData);
|
|
2861
|
+
}
|
|
2862
|
+
catch {
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2827
2866
|
/**
|
|
2828
2867
|
* Same JSON body walk as {@link jsonBodyLeafValues}, but grouped by the JSON
|
|
2829
2868
|
* key/array-index that carries each leaf value — the by-name correlation
|
|
@@ -4031,7 +4070,28 @@ function containsValueAtTokenBoundary(haystack, needle) {
|
|
|
4031
4070
|
from = at + needle.length;
|
|
4032
4071
|
}
|
|
4033
4072
|
}
|
|
4034
|
-
|
|
4073
|
+
const structuredFieldSamples = new WeakMap();
|
|
4074
|
+
/**
|
|
4075
|
+
* Single registration point for a structured (array/object) payload field, so
|
|
4076
|
+
* the declared schema type is inferred from every captured body carrying the
|
|
4077
|
+
* field rather than from whichever body or visiting order reached it first.
|
|
4078
|
+
* Both the by-index accessor loop and the whole-field substitution pass go
|
|
4079
|
+
* through here, so an indexed accessor can never coexist with a string schema.
|
|
4080
|
+
*/
|
|
4081
|
+
function registerStructuredPayloadField(outStructuredKeys, field, value) {
|
|
4082
|
+
if (value === undefined || value === null)
|
|
4083
|
+
return;
|
|
4084
|
+
const samplesByField = structuredFieldSamples.get(outStructuredKeys) ?? new Map();
|
|
4085
|
+
structuredFieldSamples.set(outStructuredKeys, samplesByField);
|
|
4086
|
+
const samples = samplesByField.get(field) ?? [];
|
|
4087
|
+
samplesByField.set(field, samples);
|
|
4088
|
+
const serialized = JSON.stringify(value);
|
|
4089
|
+
if (samples.some((sample) => JSON.stringify(sample) === serialized))
|
|
4090
|
+
return;
|
|
4091
|
+
samples.push(value);
|
|
4092
|
+
outStructuredKeys.set(field, inferZodSchemaFromSamples(samples));
|
|
4093
|
+
}
|
|
4094
|
+
function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, rootBody, payloadAccessorExcludePattern, ownedArrayPathKeys) {
|
|
4035
4095
|
let result = template;
|
|
4036
4096
|
let cursor = searchFrom;
|
|
4037
4097
|
// The body root's own entries are eligible too, whenever the located
|
|
@@ -4041,11 +4101,11 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4041
4101
|
// pagination/sort block) outranks the root as the "form envelope". Root
|
|
4042
4102
|
// keys already present on the envelope are skipped so nothing is visited
|
|
4043
4103
|
// (or registered) twice.
|
|
4044
|
-
const envelopeEntries = Object.entries(envelope);
|
|
4045
|
-
const rootEntries = envelopePath.length === 0
|
|
4104
|
+
const envelopeEntries = Object.entries(envelope).map(([key, value]) => [key, value, [...envelopePath, key]]);
|
|
4105
|
+
const rootEntries = (envelopePath.length === 0
|
|
4046
4106
|
? []
|
|
4047
|
-
: Object.entries(rootBody).filter(([key]) => key !== envelopePath[0] && !(key in envelope));
|
|
4048
|
-
for (const [key, value] of [...envelopeEntries, ...rootEntries]) {
|
|
4107
|
+
: Object.entries(rootBody).filter(([key]) => key !== envelopePath[0] && !(key in envelope))).map(([key, value]) => [key, value, [key]]);
|
|
4108
|
+
for (const [key, value, valuePath] of [...envelopeEntries, ...rootEntries]) {
|
|
4049
4109
|
const isNonEmptyArray = Array.isArray(value) && value.length > 0;
|
|
4050
4110
|
const isNestedObject = value !== null &&
|
|
4051
4111
|
typeof value === "object" &&
|
|
@@ -4053,6 +4113,22 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4053
4113
|
Object.keys(value).length > 0;
|
|
4054
4114
|
if (!isNonEmptyArray && !isNestedObject)
|
|
4055
4115
|
continue;
|
|
4116
|
+
// An array path the facet pass owns is rendered element by element at every
|
|
4117
|
+
// call site, so it is neither a payload field of its own nor swallowable.
|
|
4118
|
+
if (isNonEmptyArray && ownedArrayPathKeys.has(arrayPathKey(valuePath)))
|
|
4119
|
+
continue;
|
|
4120
|
+
// Schema registration and the wholesale text-swallow below are
|
|
4121
|
+
// independent concerns: this field's inferred Zod shape is correct
|
|
4122
|
+
// regardless of whether any of the exclusion/guard checks below skip
|
|
4123
|
+
// freezing its literal into the template. Registering it here (before
|
|
4124
|
+
// those checks, and before even locating the span) keeps the generated
|
|
4125
|
+
// contract's declared type in sync with payloadAccessorForPath's
|
|
4126
|
+
// array/object accessors even when the wholesale swallow is skipped.
|
|
4127
|
+
registerStructuredPayloadField(outStructuredKeys, key, value);
|
|
4128
|
+
// A call site whose array carries a facet was already rewritten by the
|
|
4129
|
+
// facet pass (the span-text guard below leaves it intact); a site whose
|
|
4130
|
+
// array carries none falls through to the same opaque `payload.<key>`
|
|
4131
|
+
// passthrough every non-facet array gets, so the declared field is used.
|
|
4056
4132
|
if (unconditionalExcludeValues.size > 0 ||
|
|
4057
4133
|
restrictedExcludeSourceByValue.size > 0 ||
|
|
4058
4134
|
payloadAccessorExcludePattern !== null) {
|
|
@@ -4162,9 +4238,6 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4162
4238
|
const replacement = `$${"{"}JSON.stringify(payload.${key})${"}"}`;
|
|
4163
4239
|
result = result.slice(0, spanStart) + replacement + result.slice(spanEnd);
|
|
4164
4240
|
cursor = Math.max(cursor, spanStart + replacement.length);
|
|
4165
|
-
if (!outStructuredKeys.has(key)) {
|
|
4166
|
-
outStructuredKeys.set(key, inferZodSchema(value));
|
|
4167
|
-
}
|
|
4168
4241
|
}
|
|
4169
4242
|
return { result, nextSearchFrom: cursor };
|
|
4170
4243
|
}
|
|
@@ -4174,7 +4247,7 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
4174
4247
|
* pass to it. Shared by the top-level-object and top-level-array branches of
|
|
4175
4248
|
* {@link applyStructuredValuePayloadSubstitutions} below.
|
|
4176
4249
|
*/
|
|
4177
|
-
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, payloadAccessorExcludePattern) {
|
|
4250
|
+
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, payloadAccessorExcludePattern, ownedArrayPathKeys) {
|
|
4178
4251
|
const envelopePath = locateFormEnvelopePath(objectBody);
|
|
4179
4252
|
let envelope = objectBody;
|
|
4180
4253
|
for (const seg of envelopePath) {
|
|
@@ -4185,7 +4258,7 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4185
4258
|
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
4186
4259
|
return { result: template, nextSearchFrom: searchFrom };
|
|
4187
4260
|
}
|
|
4188
|
-
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody, payloadAccessorExcludePattern);
|
|
4261
|
+
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody, payloadAccessorExcludePattern, ownedArrayPathKeys);
|
|
4189
4262
|
}
|
|
4190
4263
|
/**
|
|
4191
4264
|
* `priorStepStateBindings` carries the SAME value→binding map
|
|
@@ -4210,7 +4283,7 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4210
4283
|
* excluded from threading — the same value-coincidence class of bug
|
|
4211
4284
|
* {@link keyNamesCorrelate}'s other call sites already guard against.
|
|
4212
4285
|
*/
|
|
4213
|
-
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set(), payloadAccessorExcludeValues = new Map()) {
|
|
4286
|
+
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set(), payloadAccessorExcludeValues = new Map(), ownedArrayPathKeys = new Set()) {
|
|
4214
4287
|
if (parsedBody === null || typeof parsedBody !== "object") {
|
|
4215
4288
|
return template;
|
|
4216
4289
|
}
|
|
@@ -4237,13 +4310,13 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
|
|
|
4237
4310
|
for (const element of parsedBody) {
|
|
4238
4311
|
if (element === null || typeof element !== "object" || Array.isArray(element))
|
|
4239
4312
|
continue;
|
|
4240
|
-
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor, payloadAccessorExcludePattern);
|
|
4313
|
+
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor, payloadAccessorExcludePattern, ownedArrayPathKeys);
|
|
4241
4314
|
result = nextResult;
|
|
4242
4315
|
cursor = nextSearchFrom;
|
|
4243
4316
|
}
|
|
4244
4317
|
return result;
|
|
4245
4318
|
}
|
|
4246
|
-
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0, payloadAccessorExcludePattern);
|
|
4319
|
+
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0, payloadAccessorExcludePattern, ownedArrayPathKeys);
|
|
4247
4320
|
return result;
|
|
4248
4321
|
}
|
|
4249
4322
|
/** Maximum length to guard against indexing massive blobs (HTML fragments,
|
|
@@ -4508,12 +4581,22 @@ function payloadAccessorForPath(path) {
|
|
|
4508
4581
|
const arrayIndexPos = path.findIndex((segment) => /^\d+$/.test(segment));
|
|
4509
4582
|
if (arrayIndexPos === -1) {
|
|
4510
4583
|
const field = pathToPayloadFieldName(path);
|
|
4511
|
-
return { accessor: `payload.${field}`, field };
|
|
4584
|
+
return { accessor: `payload.${field}`, field, structuredRootPath: null };
|
|
4512
4585
|
}
|
|
4513
4586
|
const objectPath = path.slice(0, arrayIndexPos);
|
|
4514
4587
|
const field = objectPath.length > 0 ? pathToPayloadFieldName(objectPath) : (path[0] ?? "");
|
|
4515
4588
|
const suffix = pathToAccessor(path.slice(arrayIndexPos), { assertNonNull: true });
|
|
4516
|
-
return {
|
|
4589
|
+
return {
|
|
4590
|
+
accessor: `payload.${field}${suffix}`,
|
|
4591
|
+
field,
|
|
4592
|
+
structuredRootPath: objectPath.length > 0 ? objectPath : null,
|
|
4593
|
+
};
|
|
4594
|
+
}
|
|
4595
|
+
/** Reads the value at `path` inside a parsed JSON body, or undefined when any hop is missing. */
|
|
4596
|
+
function valueAtJsonPath(root, path) {
|
|
4597
|
+
return path.reduce((node, segment) => node !== null && typeof node === "object"
|
|
4598
|
+
? node[segment]
|
|
4599
|
+
: undefined, root);
|
|
4517
4600
|
}
|
|
4518
4601
|
/** Derives a valid camelCase identifier from a fixture filename (e.g.
|
|
4519
4602
|
* "10219132.json" -> "fixture10219132", "acme-metrics.config.json" ->
|
|
@@ -5686,12 +5769,29 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
|
|
|
5686
5769
|
/** Every capture's response body from the same run, scanned only for
|
|
5687
5770
|
* {@link CAPACITY_FIELD_NAME_PATTERN}-shaped numeric leaves — the
|
|
5688
5771
|
* evidence source for a discovered "number" key's `capacityMax`. */
|
|
5689
|
-
responseBodiesForCapacitySignal = []) {
|
|
5772
|
+
responseBodiesForCapacitySignal = [], outStructuredKeys = new Map()) {
|
|
5690
5773
|
const merged = [];
|
|
5691
5774
|
const seenPairs = new Set();
|
|
5692
5775
|
const seenValueByKey = new Map();
|
|
5693
5776
|
const distinctValuesByKey = new Map();
|
|
5694
5777
|
const allBodies = [inputBody, ...additionalBodies];
|
|
5778
|
+
// Structure is registered from every body before any scalar is considered, so
|
|
5779
|
+
// a name that is an array/object in one body and a primitive in another gets
|
|
5780
|
+
// the structured schema regardless of which body is visited first.
|
|
5781
|
+
for (const body of allBodies) {
|
|
5782
|
+
if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
5783
|
+
continue;
|
|
5784
|
+
}
|
|
5785
|
+
for (const [key, value] of Object.entries(body)) {
|
|
5786
|
+
if (!isValidJsIdentifier(key))
|
|
5787
|
+
continue;
|
|
5788
|
+
if (value === null || typeof value !== "object")
|
|
5789
|
+
continue;
|
|
5790
|
+
if (Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0)
|
|
5791
|
+
continue;
|
|
5792
|
+
registerStructuredPayloadField(outStructuredKeys, key, value);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5695
5795
|
for (const body of allBodies) {
|
|
5696
5796
|
if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
5697
5797
|
continue;
|
|
@@ -5702,6 +5802,8 @@ responseBodiesForCapacitySignal = []) {
|
|
|
5702
5802
|
const key = path[0];
|
|
5703
5803
|
if (!isValidJsIdentifier(key))
|
|
5704
5804
|
continue;
|
|
5805
|
+
if (outStructuredKeys.has(key))
|
|
5806
|
+
continue;
|
|
5705
5807
|
if (value === null)
|
|
5706
5808
|
continue;
|
|
5707
5809
|
// Dedupe identical (key, value) pairs only — a repeated occurrence of
|
|
@@ -5870,17 +5972,57 @@ responseBodiesForCapacitySignal = []) {
|
|
|
5870
5972
|
* steps declared any field to correlate against) or when a leaf's value has
|
|
5871
5973
|
* no `key:value` segment matching one of them.
|
|
5872
5974
|
*/
|
|
5873
|
-
/**
|
|
5874
|
-
*
|
|
5875
|
-
*
|
|
5876
|
-
*
|
|
5877
|
-
*
|
|
5878
|
-
*
|
|
5975
|
+
/**
|
|
5976
|
+
* Binds a scalar navigateTo facet at every call site: each object-property
|
|
5977
|
+
* string leaf, at any depth, whose whole value equals a declared facet literal
|
|
5978
|
+
* becomes `${payload.<field>}`. Matching is per whole leaf, so the same
|
|
5979
|
+
* literal flanked by alphanumerics inside an unrelated token is never touched
|
|
5980
|
+
* and no global collision guard can drop the facet. An optional facet falls
|
|
5981
|
+
* back to the captured literal when the caller omits it, never `undefined`.
|
|
5982
|
+
*/
|
|
5983
|
+
function applyScalarFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets) {
|
|
5984
|
+
if (navigateToFacets.length === 0)
|
|
5985
|
+
return template;
|
|
5986
|
+
if (inputBody === null || typeof inputBody !== "object")
|
|
5987
|
+
return template;
|
|
5988
|
+
let result = template;
|
|
5989
|
+
for (const { value, path } of walkAllPrimitiveLeaves(inputBody)) {
|
|
5990
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
5991
|
+
continue;
|
|
5992
|
+
if (/^\d+$/.test(path[path.length - 1] ?? ""))
|
|
5993
|
+
continue;
|
|
5994
|
+
const facet = matchFacetRecurrence(String(value), navigateToFacets, {
|
|
5995
|
+
allowDelimiterSuffix: false,
|
|
5996
|
+
});
|
|
5997
|
+
if (facet === undefined)
|
|
5998
|
+
continue;
|
|
5999
|
+
const literal = JSON.stringify(value);
|
|
6000
|
+
const fallback = facet.optional ? ` ?? ${literal}` : "";
|
|
6001
|
+
const replacement = typeof value === "number" || facet.optional
|
|
6002
|
+
? `\${JSON.stringify(payload.${facet.field}${fallback})}`
|
|
6003
|
+
: `"\${payload.${facet.field}}"`;
|
|
6004
|
+
for (const sep of [":", ": "]) {
|
|
6005
|
+
if (typeof value === "number") {
|
|
6006
|
+
const re = new RegExp(`${sep}${literal}(?![\\w.])`, "g");
|
|
6007
|
+
result = result.replace(re, () => `${sep}${replacement}`);
|
|
6008
|
+
continue;
|
|
6009
|
+
}
|
|
6010
|
+
result = result.split(`${sep}${literal}`).join(`${sep}${replacement}`);
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
6013
|
+
return result;
|
|
6014
|
+
}
|
|
6015
|
+
/** Yields every array in `value` (outermost first, at any nesting depth,
|
|
6016
|
+
* including arrays inside array elements) — `{@link
|
|
6017
|
+
* applyArrayFacetSplicePayloadSubstitutions}`'s raw-array search surface. */
|
|
5879
6018
|
function* walkArrayLeaves(value, path = []) {
|
|
5880
6019
|
if (value === null || typeof value !== "object")
|
|
5881
6020
|
return;
|
|
5882
6021
|
if (Array.isArray(value)) {
|
|
5883
6022
|
yield { value, path };
|
|
6023
|
+
for (const [index, inner] of value.entries()) {
|
|
6024
|
+
yield* walkArrayLeaves(inner, [...path, String(index)]);
|
|
6025
|
+
}
|
|
5884
6026
|
return;
|
|
5885
6027
|
}
|
|
5886
6028
|
for (const [key, inner] of Object.entries(value)) {
|
|
@@ -5890,45 +6032,191 @@ function* walkArrayLeaves(value, path = []) {
|
|
|
5890
6032
|
/**
|
|
5891
6033
|
* REST-body counterpart to {@link spliceFacetsIntoArrayVariable}'s GraphQL
|
|
5892
6034
|
* 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
|
|
5894
|
-
*
|
|
5895
|
-
*
|
|
5896
|
-
*
|
|
5897
|
-
*
|
|
5898
|
-
*
|
|
5899
|
-
* element
|
|
6035
|
+
* by a constant delimiter) inside a REST JSON body gets its element swapped
|
|
6036
|
+
* for `${payload.<field>}` — for EVERY declared facet, required or optional,
|
|
6037
|
+
* at EVERY call site's own array. Matching is structural (see
|
|
6038
|
+
* {@link replaceJsonArrayOccurrences}) and token-boundary guarded per element
|
|
6039
|
+
* ({@link matchArrayElementFacet}), so the splice never depends on the
|
|
6040
|
+
* global, all-or-nothing persona collision guard, on which call site carried
|
|
6041
|
+
* the array first, on element order, or on a facet being optional.
|
|
5900
6042
|
*
|
|
5901
|
-
*
|
|
5902
|
-
*
|
|
5903
|
-
*
|
|
5904
|
-
*
|
|
5905
|
-
*
|
|
5906
|
-
*
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
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))
|
|
6043
|
+
* Every facet-carrying array, whether its facets are required or optional, is
|
|
6044
|
+
* rewritten to the same single `${JSON.stringify([...])}` splice point built
|
|
6045
|
+
* from {@link spliceFacetsIntoArrayVariable}'s per-element conditional
|
|
6046
|
+
* emission, so an absent optional facet drops its element instead of freezing
|
|
6047
|
+
* an `undefined` and no call site renders the array through a different
|
|
6048
|
+
* construction. Non-facet elements stay literal.
|
|
6049
|
+
*/
|
|
6050
|
+
function applyArrayFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets, ownedArrayPathKeys, siblingBodies = []) {
|
|
6051
|
+
if (navigateToFacets.length === 0)
|
|
5915
6052
|
return template;
|
|
5916
6053
|
if (inputBody === null || typeof inputBody !== "object")
|
|
5917
6054
|
return template;
|
|
5918
6055
|
let result = template;
|
|
5919
|
-
for (const { value } of walkArrayLeaves(inputBody)) {
|
|
5920
|
-
|
|
5921
|
-
if (spliced === null || !spliced.includes("...(payload."))
|
|
6056
|
+
for (const { value, path } of walkArrayLeaves(inputBody)) {
|
|
6057
|
+
if (value.length === 0)
|
|
5922
6058
|
continue;
|
|
5923
|
-
|
|
5924
|
-
|
|
6059
|
+
if (!ownedArrayPathKeys.has(arrayPathKey(path)) &&
|
|
6060
|
+
!containsFacetArray(value, navigateToFacets)) {
|
|
5925
6061
|
continue;
|
|
5926
|
-
|
|
6062
|
+
}
|
|
6063
|
+
const absentFacets = collectSiblingArrayFacets(value, path, siblingBodies, navigateToFacets);
|
|
6064
|
+
const absentFields = new Set(absentFacets.map(({ facet }) => facet.field));
|
|
6065
|
+
const expression = renderFacetValueExpression([...value, ...absentFacets.map(({ element }) => element)], navigateToFacets.map((facet) => absentFields.has(facet.field) ? { ...facet, optional: true } : facet));
|
|
6066
|
+
result = replaceJsonArrayOccurrences(result, value, `\${JSON.stringify(${expression})}`);
|
|
5927
6067
|
}
|
|
5928
6068
|
return result;
|
|
5929
6069
|
}
|
|
5930
|
-
|
|
5931
|
-
|
|
6070
|
+
/**
|
|
6071
|
+
* Facet-bearing elements that same-endpoint sibling captures carry at the
|
|
6072
|
+
* array `path` but `own` does not. A fold drill re-issues ONE representative
|
|
6073
|
+
* capture per item, so a facet only a dropped sibling's differently shaped
|
|
6074
|
+
* array carried would otherwise never reach the emitted body. Each facet is
|
|
6075
|
+
* taken once, at its first occurrence, and only for facets `own` leaves out.
|
|
6076
|
+
*/
|
|
6077
|
+
function collectSiblingArrayFacets(own, path, siblingBodies, facets) {
|
|
6078
|
+
const present = new Set(own.flatMap((element) => typeof element === "string" ? [matchArrayElementFacet(element, facets)?.field] : []));
|
|
6079
|
+
const key = arrayPathKey(path);
|
|
6080
|
+
const absent = [];
|
|
6081
|
+
for (const body of siblingBodies) {
|
|
6082
|
+
if (body === null || typeof body !== "object")
|
|
6083
|
+
continue;
|
|
6084
|
+
for (const { value, path: siblingPath } of walkArrayLeaves(body)) {
|
|
6085
|
+
if (arrayPathKey(siblingPath) !== key)
|
|
6086
|
+
continue;
|
|
6087
|
+
for (const element of value) {
|
|
6088
|
+
if (typeof element !== "string")
|
|
6089
|
+
continue;
|
|
6090
|
+
const facet = matchArrayElementFacet(element, facets);
|
|
6091
|
+
if (facet === undefined || present.has(facet.field))
|
|
6092
|
+
continue;
|
|
6093
|
+
present.add(facet.field);
|
|
6094
|
+
absent.push({ element, facet });
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
return absent;
|
|
6099
|
+
}
|
|
6100
|
+
/** Whether `value` is, or contains at any depth, an array with a
|
|
6101
|
+
* facet-bearing string element. */
|
|
6102
|
+
function containsFacetArray(value, facets) {
|
|
6103
|
+
for (const { value: array } of walkArrayLeaves(value)) {
|
|
6104
|
+
const hasFacet = array.some((element) => typeof element === "string" && matchArrayElementFacet(element, facets));
|
|
6105
|
+
if (hasFacet)
|
|
6106
|
+
return true;
|
|
6107
|
+
}
|
|
6108
|
+
return false;
|
|
6109
|
+
}
|
|
6110
|
+
/** JS expression for `value` with every facet array at any depth spliced, so
|
|
6111
|
+
* one construction covers arrays nested inside array elements. */
|
|
6112
|
+
function renderFacetValueExpression(value, facets) {
|
|
6113
|
+
if (value === null || typeof value !== "object" || !containsFacetArray(value, facets)) {
|
|
6114
|
+
return JSON.stringify(value);
|
|
6115
|
+
}
|
|
6116
|
+
if (Array.isArray(value)) {
|
|
6117
|
+
return (spliceFacetsIntoArrayVariable(value, facets, (element) => renderFacetValueExpression(element, facets)) ?? `[${value.map((element) => renderFacetValueExpression(element, facets)).join(", ")}]`);
|
|
6118
|
+
}
|
|
6119
|
+
const entries = Object.entries(value).map(([key, inner]) => `${JSON.stringify(key)}: ${renderFacetValueExpression(inner, facets)}`);
|
|
6120
|
+
return `{ ${entries.join(", ")} }`;
|
|
6121
|
+
}
|
|
6122
|
+
/** Full index-free path of an array: identifies the array across call sites
|
|
6123
|
+
* whatever index it was captured at, without conflating same-named fields
|
|
6124
|
+
* that live at different paths. */
|
|
6125
|
+
function arrayPathKey(path) {
|
|
6126
|
+
return JSON.stringify(path.filter((segment) => !/^\d+$/.test(segment)));
|
|
6127
|
+
}
|
|
6128
|
+
/** True when `path` addresses anything inside an array in `ownedKeys`, at any
|
|
6129
|
+
* depth (string or object element alike). */
|
|
6130
|
+
function isInsideOwnedArray(path, ownedKeys) {
|
|
6131
|
+
return path.some((segment, index) => /^\d+$/.test(segment) && ownedKeys.has(arrayPathKey(path.slice(0, index))));
|
|
6132
|
+
}
|
|
6133
|
+
/**
|
|
6134
|
+
* Path keys ({@link arrayPathKey}) of every array, in ANY captured body,
|
|
6135
|
+
* that carries at least one facet-bearing element. The facet pass owns those
|
|
6136
|
+
* arrays wholesale, so none of their elements may also be frozen as a
|
|
6137
|
+
* by-index `payload.<field>["N"]` accessor, whichever body was parsed first
|
|
6138
|
+
* and wherever the array sits in it.
|
|
6139
|
+
*/
|
|
6140
|
+
function collectFacetArrayPathKeys(bodies, facets) {
|
|
6141
|
+
const keys = new Set();
|
|
6142
|
+
if (facets.length === 0)
|
|
6143
|
+
return keys;
|
|
6144
|
+
for (const body of bodies) {
|
|
6145
|
+
if (body === null || typeof body !== "object")
|
|
6146
|
+
continue;
|
|
6147
|
+
for (const { value, path } of walkArrayLeaves(body)) {
|
|
6148
|
+
const hasFacet = value.some((element) => typeof element === "string" && matchArrayElementFacet(element, facets) !== undefined);
|
|
6149
|
+
if (hasFacet)
|
|
6150
|
+
keys.add(arrayPathKey(path));
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
return keys;
|
|
6154
|
+
}
|
|
6155
|
+
/**
|
|
6156
|
+
* Rewrites every occurrence of `target` inside `template` by JSON structure
|
|
6157
|
+
* (bracket-balanced, string-aware, whitespace-insensitive) rather than by
|
|
6158
|
+
* exact `JSON.stringify` text, so a pretty-printed or re-spaced body array
|
|
6159
|
+
* is still replaced.
|
|
6160
|
+
*/
|
|
6161
|
+
function replaceJsonArrayOccurrences(template, target, replacement) {
|
|
6162
|
+
const targetKey = JSON.stringify(target);
|
|
6163
|
+
let out = "";
|
|
6164
|
+
let from = 0;
|
|
6165
|
+
while (from < template.length) {
|
|
6166
|
+
const open = template.indexOf("[", from);
|
|
6167
|
+
if (open === -1)
|
|
6168
|
+
break;
|
|
6169
|
+
const close = findMatchingBracket(template, open);
|
|
6170
|
+
const candidate = close === -1 ? undefined : template.slice(open, close + 1);
|
|
6171
|
+
const matches = candidate !== undefined && jsonTextEquals(candidate, targetKey);
|
|
6172
|
+
if (!matches) {
|
|
6173
|
+
out += template.slice(from, open + 1);
|
|
6174
|
+
from = open + 1;
|
|
6175
|
+
continue;
|
|
6176
|
+
}
|
|
6177
|
+
out += template.slice(from, open) + replacement;
|
|
6178
|
+
from = close + 1;
|
|
6179
|
+
}
|
|
6180
|
+
return out + template.slice(from);
|
|
6181
|
+
}
|
|
6182
|
+
function findMatchingBracket(text, open) {
|
|
6183
|
+
let depth = 0;
|
|
6184
|
+
let inString = false;
|
|
6185
|
+
for (let i = open; i < text.length; i++) {
|
|
6186
|
+
const ch = text[i];
|
|
6187
|
+
if (inString) {
|
|
6188
|
+
if (ch === "\\")
|
|
6189
|
+
i++;
|
|
6190
|
+
else if (ch === '"')
|
|
6191
|
+
inString = false;
|
|
6192
|
+
continue;
|
|
6193
|
+
}
|
|
6194
|
+
if (ch === '"')
|
|
6195
|
+
inString = true;
|
|
6196
|
+
else if (ch === "[")
|
|
6197
|
+
depth++;
|
|
6198
|
+
else if (ch === "]" && --depth === 0)
|
|
6199
|
+
return i;
|
|
6200
|
+
}
|
|
6201
|
+
return -1;
|
|
6202
|
+
}
|
|
6203
|
+
function jsonTextEquals(candidate, targetKey) {
|
|
6204
|
+
try {
|
|
6205
|
+
return JSON.stringify(JSON.parse(candidate)) === targetKey;
|
|
6206
|
+
}
|
|
6207
|
+
catch {
|
|
6208
|
+
return false;
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
/** A facet literal recurring inside a packed string under a key that does not
|
|
6212
|
+
* name its payload field. A leaf that IS the bare literal is left to the scalar
|
|
6213
|
+
* pass, which owns the optional fallback for it. */
|
|
6214
|
+
function packedFacetRecurrence(value, facets) {
|
|
6215
|
+
const spliced = spliceFacetRecurrenceIntoScalarVariable(value, facets);
|
|
6216
|
+
return spliced !== null && !spliced.startsWith("payload.") ? spliced : null;
|
|
6217
|
+
}
|
|
6218
|
+
function applyFacetSplicePayloadSubstitutions(template, inputBody, payloadFieldNames, optionalFieldNames, navigateToFacets = []) {
|
|
6219
|
+
if (payloadFieldNames.size === 0 && navigateToFacets.length === 0)
|
|
5932
6220
|
return template;
|
|
5933
6221
|
if (inputBody === undefined ||
|
|
5934
6222
|
inputBody === null ||
|
|
@@ -5939,18 +6227,23 @@ function applyFacetSplicePayloadSubstitutions(template, inputBody, payloadFieldN
|
|
|
5939
6227
|
const fields = [...payloadFieldNames];
|
|
5940
6228
|
let result = template;
|
|
5941
6229
|
for (const { value, path } of walkAllPrimitiveLeaves(inputBody)) {
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
const key = path[0];
|
|
5945
|
-
if (!isValidJsIdentifier(key))
|
|
6230
|
+
const key = path[path.length - 1];
|
|
6231
|
+
if (key === undefined || !isValidJsIdentifier(key))
|
|
5946
6232
|
continue;
|
|
5947
6233
|
if (typeof value !== "string")
|
|
5948
6234
|
continue;
|
|
5949
|
-
const
|
|
6235
|
+
const kv = spliceFacetsIntoStringVariable(value, fields, optionalFieldNames);
|
|
6236
|
+
const spliced = kv ?? packedFacetRecurrence(value, navigateToFacets);
|
|
5950
6237
|
if (spliced === null)
|
|
5951
6238
|
continue;
|
|
5952
6239
|
const target = `"${key}":${JSON.stringify(value)}`;
|
|
5953
|
-
|
|
6240
|
+
// A required-only packed value with nothing JSON-escaped splices in place,
|
|
6241
|
+
// so the accessor sits directly in the string instead of nesting a template.
|
|
6242
|
+
const inline = kv === null &&
|
|
6243
|
+
spliced.startsWith("`") &&
|
|
6244
|
+
!spliced.startsWith("`${") &&
|
|
6245
|
+
JSON.stringify(value).slice(1, -1) === value;
|
|
6246
|
+
const replacement = inline ? `"${key}":"${spliced.slice(1, -1)}"` : `"${key}":"\${${spliced}}"`;
|
|
5954
6247
|
result = result.split(target).join(replacement);
|
|
5955
6248
|
}
|
|
5956
6249
|
return result;
|
|
@@ -6231,8 +6524,12 @@ function parseBodyLeafValues(requestPostData) {
|
|
|
6231
6524
|
* affect (the already-working recurrence-anchored path exists precisely
|
|
6232
6525
|
* because a real facet DOES recur in a later request; nothing here changes
|
|
6233
6526
|
* that — this only supplies values for facets that are not found at all).
|
|
6527
|
+
*
|
|
6528
|
+
* Values that carry a literal of an already-reachable facet are explained by
|
|
6529
|
+
* that facet and never enter the pool, so reachable facets' own transitions
|
|
6530
|
+
* cannot skew the pool size away from the count of facets awaiting a value.
|
|
6234
6531
|
*/
|
|
6235
|
-
function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
6532
|
+
function collectNewlyAppearingRequestValueTransitions(actions, explainedLiterals = []) {
|
|
6236
6533
|
const seenGlobally = new Set();
|
|
6237
6534
|
const transitions = [];
|
|
6238
6535
|
for (let i = 1; i < actions.length; i++) {
|
|
@@ -6243,6 +6540,8 @@ function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
|
6243
6540
|
const record = (value) => {
|
|
6244
6541
|
if (value.length === 0 || seenGlobally.has(value) || seenThisTransition.has(value))
|
|
6245
6542
|
return;
|
|
6543
|
+
if (explainedLiterals.some((literal) => occursAtTokenBoundary(value, literal)))
|
|
6544
|
+
return;
|
|
6246
6545
|
seenThisTransition.add(value);
|
|
6247
6546
|
values.push(value);
|
|
6248
6547
|
};
|
|
@@ -6263,6 +6562,79 @@ function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
|
6263
6562
|
}
|
|
6264
6563
|
return transitions;
|
|
6265
6564
|
}
|
|
6565
|
+
/**
|
|
6566
|
+
* Whether `value` occurs in every request body only at token boundaries (never
|
|
6567
|
+
* flanked by an alphanumeric), so a global rewrite of it cannot mangle a longer
|
|
6568
|
+
* token. Shared by the persona-binding guard and facet identity resolution.
|
|
6569
|
+
*/
|
|
6570
|
+
function bindsWithoutCollisionIn(bodies, value) {
|
|
6571
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
6572
|
+
for (const body of bodies) {
|
|
6573
|
+
let from = 0;
|
|
6574
|
+
while (true) {
|
|
6575
|
+
const at = body.indexOf(value, from);
|
|
6576
|
+
if (at === -1)
|
|
6577
|
+
break;
|
|
6578
|
+
if (isAlnum(body[at - 1]) || isAlnum(body[at + value.length]))
|
|
6579
|
+
return false;
|
|
6580
|
+
from = at + value.length;
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
return true;
|
|
6584
|
+
}
|
|
6585
|
+
/**
|
|
6586
|
+
* Whether `value` occurs in `text` at least once flanked by non-alphanumerics,
|
|
6587
|
+
* so facet reachability agrees with the token-boundary {@link matchFacetRecurrence}
|
|
6588
|
+
* the splice passes use instead of counting a mere substring of a longer token.
|
|
6589
|
+
*/
|
|
6590
|
+
function occursAtTokenBoundary(text, value) {
|
|
6591
|
+
if (value.length === 0)
|
|
6592
|
+
return false;
|
|
6593
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
6594
|
+
let from = 0;
|
|
6595
|
+
while (true) {
|
|
6596
|
+
const at = text.indexOf(value, from);
|
|
6597
|
+
if (at === -1)
|
|
6598
|
+
return false;
|
|
6599
|
+
if (!isAlnum(text[at - 1]) && !isAlnum(text[at + value.length]))
|
|
6600
|
+
return true;
|
|
6601
|
+
from = at + 1;
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
/**
|
|
6605
|
+
* Every literal the flow's declared navigateTo facets take in the captured
|
|
6606
|
+
* traffic: the hash-derived literal plus, for a facet whose hash literal never
|
|
6607
|
+
* recurs, the body/query value {@link correlateUnreachableNavigateToFacets}
|
|
6608
|
+
* pairs it with. Every per-site splice pass matches against this one list, so
|
|
6609
|
+
* a facet is judged to recur identically by all of them instead of each pass
|
|
6610
|
+
* deciding from the hash-derived literal alone.
|
|
6611
|
+
*/
|
|
6612
|
+
function resolveRecurringNavigateToFacets(declared, actions) {
|
|
6613
|
+
const appearsAnywhere = (value) => actions.some(({ capture }) => occursAtTokenBoundary(capture.url, value) ||
|
|
6614
|
+
(capture.requestPostData != null &&
|
|
6615
|
+
occursAtTokenBoundary(capture.requestPostData, value)) ||
|
|
6616
|
+
Object.values(capture.requestHeaders).some((h) => occursAtTokenBoundary(h, value)));
|
|
6617
|
+
const reachable = declared.filter(({ value }) => value.length > 0 && appearsAnywhere(value));
|
|
6618
|
+
const resolvedDeclared = declared.filter(({ value }) => value.length > 0);
|
|
6619
|
+
const unreachable = declared.filter((facet) => !reachable.includes(facet));
|
|
6620
|
+
if (unreachable.length === 0)
|
|
6621
|
+
return resolvedDeclared;
|
|
6622
|
+
const correlated = correlateUnreachableNavigateToFacets(unreachable, actions, reachable.map(({ value }) => value));
|
|
6623
|
+
if (correlated === null)
|
|
6624
|
+
return resolvedDeclared;
|
|
6625
|
+
const bodies = actions
|
|
6626
|
+
.map(({ capture }) => capture.requestPostData)
|
|
6627
|
+
.filter((body) => typeof body === "string" && body.length > 0);
|
|
6628
|
+
const extras = unreachable.flatMap((facet) => {
|
|
6629
|
+
const diffed = correlated.get(facet.field);
|
|
6630
|
+
if (diffed === undefined || diffed.length === 0)
|
|
6631
|
+
return [];
|
|
6632
|
+
if (declared.some(({ value }) => value === diffed))
|
|
6633
|
+
return [];
|
|
6634
|
+
return bindsWithoutCollisionIn(bodies, diffed) ? [{ ...facet, value: diffed }] : [];
|
|
6635
|
+
});
|
|
6636
|
+
return [...resolvedDeclared, ...extras];
|
|
6637
|
+
}
|
|
6266
6638
|
/**
|
|
6267
6639
|
* Resolves each zero-recurrence navigateTo facet (in flow order) to the real
|
|
6268
6640
|
* request value it causally affects, or returns `null` when the correlation
|
|
@@ -6279,14 +6651,27 @@ function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
|
6279
6651
|
* no longer guaranteed to line facets up with the RIGHT transition, so
|
|
6280
6652
|
* nothing is bound — leaving a facet declared-but-unbound is preferable to
|
|
6281
6653
|
* silently wiring it to another facet's (or an unrelated transition's) value.
|
|
6654
|
+
*
|
|
6655
|
+
* When the TOTAL transition count (ambiguous ones included) equals the facet
|
|
6656
|
+
* count, each ambiguous transition still holds its facet's slot, so only that
|
|
6657
|
+
* facet stays unbound while the others bind by position.
|
|
6282
6658
|
*/
|
|
6283
|
-
function correlateUnreachableNavigateToFacets(facets, actions) {
|
|
6284
|
-
const
|
|
6659
|
+
function correlateUnreachableNavigateToFacets(facets, actions, explainedLiterals) {
|
|
6660
|
+
const transitions = collectNewlyAppearingRequestValueTransitions(actions, explainedLiterals);
|
|
6661
|
+
const byField = new Map();
|
|
6662
|
+
if (transitions.length === facets.length) {
|
|
6663
|
+
facets.forEach(({ field }, index) => {
|
|
6664
|
+
const transition = transitions[index];
|
|
6665
|
+
if (transition.length === 1)
|
|
6666
|
+
byField.set(field, transition[0]);
|
|
6667
|
+
});
|
|
6668
|
+
return byField.size > 0 ? byField : null;
|
|
6669
|
+
}
|
|
6670
|
+
const unambiguousValues = transitions
|
|
6285
6671
|
.filter((transition) => transition.length === 1)
|
|
6286
6672
|
.map((transition) => transition[0]);
|
|
6287
6673
|
if (unambiguousValues.length !== facets.length)
|
|
6288
6674
|
return null;
|
|
6289
|
-
const byField = new Map();
|
|
6290
6675
|
facets.forEach(({ field }, index) => {
|
|
6291
6676
|
byField.set(field, unambiguousValues[index]);
|
|
6292
6677
|
});
|
|
@@ -6333,12 +6718,30 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6333
6718
|
// below can correlate a REST body's facet-packed string leaves against it.
|
|
6334
6719
|
const { payloadFieldNames: restPayloadFieldNames, optionalPayloadFieldNames: restOptionalFieldNames, } = computeFlowPayloadFieldNames(flowSteps, vocabulary, process.env);
|
|
6335
6720
|
const payloadAccessorByValue = new Map();
|
|
6721
|
+
const declaredNavigateToFacets = extractNavigateToFacetOrder(flowSteps);
|
|
6722
|
+
const navigateToFacetOrder = resolveRecurringNavigateToFacets(declaredNavigateToFacets, actions);
|
|
6723
|
+
const facetArrayPathKeys = collectFacetArrayPathKeys(actions.map((action) => parseJsonBodyOrNull(action.capture.requestPostData)), navigateToFacetOrder);
|
|
6336
6724
|
if (inputBody !== undefined && inputBody !== null) {
|
|
6337
6725
|
for (const { value, path } of walkStringLeaves(inputBody)) {
|
|
6338
6726
|
if (value.length < MIN_STATE_VALUE_LENGTH)
|
|
6339
6727
|
continue;
|
|
6340
|
-
|
|
6728
|
+
// An element of an array any call site's facet pass owns belongs to the
|
|
6729
|
+
// facet's payloadField (or stays literal), not to a by-index accessor
|
|
6730
|
+
// that would freeze the sibling elements' shape.
|
|
6731
|
+
if (isInsideOwnedArray(path, facetArrayPathKeys))
|
|
6732
|
+
continue;
|
|
6733
|
+
// A leaf recurring as a declared facet literal binds to that facet's
|
|
6734
|
+
// payloadField via the persona pass, not to a by-index accessor.
|
|
6735
|
+
if (matchFacetRecurrence(value, navigateToFacetOrder, { allowDelimiterSuffix: true })) {
|
|
6736
|
+
continue;
|
|
6737
|
+
}
|
|
6738
|
+
const { accessor, field: accessorField, structuredRootPath } = payloadAccessorForPath(path);
|
|
6341
6739
|
payloadAccessorByValue.set(value, accessor);
|
|
6740
|
+
// The accessor indexes into this field, so its declared type must be the
|
|
6741
|
+
// structured shape regardless of whether Mechanism B visited the key.
|
|
6742
|
+
if (structuredRootPath !== null) {
|
|
6743
|
+
registerStructuredPayloadField(outStructuredKeys, accessorField, valueAtJsonPath(inputBody, structuredRootPath));
|
|
6744
|
+
}
|
|
6342
6745
|
if (isValidJsIdentifier(accessorField))
|
|
6343
6746
|
outDiscoveredFields.add(accessorField);
|
|
6344
6747
|
// Phase F: register a lowercase variant for UUID-shaped values so case-
|
|
@@ -6428,31 +6831,22 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6428
6831
|
// or when the correlation is ambiguous (see
|
|
6429
6832
|
// `correlateUnreachableNavigateToFacets`), there is nothing safe to splice
|
|
6430
6833
|
// and the field(s) stay unbound rather than risk a wrong binding.
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
const navigateToFacetOrder = extractNavigateToFacetOrder(flowSteps);
|
|
6441
|
-
const unreachableNavigateToFacets = navigateToFacetOrder.filter(({ value }) => !appearsAnywhereInCapture(value));
|
|
6442
|
-
if (unreachableNavigateToFacets.length > 0) {
|
|
6443
|
-
const correlated = correlateUnreachableNavigateToFacets(unreachableNavigateToFacets, actions);
|
|
6444
|
-
for (const { field } of correlated === null ? [] : unreachableNavigateToFacets) {
|
|
6445
|
-
const diffed = correlated?.get(field);
|
|
6446
|
-
if (diffed === undefined)
|
|
6447
|
-
continue;
|
|
6448
|
-
if (!bindsWithoutCollision(diffed))
|
|
6449
|
-
continue;
|
|
6450
|
-
if (payloadAccessorByValue.has(diffed))
|
|
6451
|
-
continue;
|
|
6452
|
-
payloadAccessorByValue.set(diffed, `payload.${field}`);
|
|
6453
|
-
if (isValidJsIdentifier(field))
|
|
6454
|
-
outDiscoveredFields.add(field);
|
|
6834
|
+
// Every facet any splice pass binds comes from the one resolved list, so the
|
|
6835
|
+
// schema declaration and accessor table cannot disagree with the splices.
|
|
6836
|
+
const declaredFacetValues = new Set(declaredNavigateToFacets.map(({ value }) => value));
|
|
6837
|
+
for (const { value, field } of navigateToFacetOrder) {
|
|
6838
|
+
if (!isValidJsIdentifier(field))
|
|
6839
|
+
continue;
|
|
6840
|
+
if (declaredFacetValues.has(value)) {
|
|
6841
|
+
outDiscoveredFields.add(field);
|
|
6842
|
+
continue;
|
|
6455
6843
|
}
|
|
6844
|
+
if (!bindsWithoutCollision(value))
|
|
6845
|
+
continue;
|
|
6846
|
+
if (payloadAccessorByValue.has(value))
|
|
6847
|
+
continue;
|
|
6848
|
+
payloadAccessorByValue.set(value, `payload.${field}`);
|
|
6849
|
+
outDiscoveredFields.add(field);
|
|
6456
6850
|
}
|
|
6457
6851
|
// Job coordinates from the recon entry URL's query string (e.g.
|
|
6458
6852
|
// `?jobSeqNo=...`). Registered the same way as BaseUrl so every verbatim
|
|
@@ -6600,6 +6994,21 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6600
6994
|
// `foldReturnSpec`, not on Pass 1's render) so Pass 1 knows which values to
|
|
6601
6995
|
// spare per step, mirroring the header-name collection above.
|
|
6602
6996
|
const joinFieldValuesByStep = new Map();
|
|
6997
|
+
// Request bodies of the absorbed repeat captures of each fold target's
|
|
6998
|
+
// representative drill (see FoldPlan.absorbedIndices): never emitted
|
|
6999
|
+
// themselves, so a facet only their differently shaped arrays carry must be
|
|
7000
|
+
// threaded through the representative's body instead of being dropped.
|
|
7001
|
+
const absorbedSiblingBodiesByStep = new Map();
|
|
7002
|
+
for (const plan of earlyFoldPlans) {
|
|
7003
|
+
for (const target of plan.targets) {
|
|
7004
|
+
const drillEndpoint = endpointKey(actions[target.drillStepIndex].capture.url);
|
|
7005
|
+
const siblings = plan.absorbedIndices
|
|
7006
|
+
.map((index) => actions[index].capture)
|
|
7007
|
+
.filter((capture) => endpointKey(capture.url) === drillEndpoint)
|
|
7008
|
+
.map((capture) => parseJsonBodyOrNull(capture.requestPostData));
|
|
7009
|
+
absorbedSiblingBodiesByStep.set(target.drillStepIndex, siblings);
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
6603
7012
|
for (const plan of earlyFoldPlans) {
|
|
6604
7013
|
const primaryItems = objectItemsAtPath(actions[plan.primaryStepIndex].capture.responseBody, plan.primaryArrayPath);
|
|
6605
7014
|
for (const target of plan.targets) {
|
|
@@ -6705,7 +7114,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6705
7114
|
// same declared field set, just applied to REST body text instead of a
|
|
6706
7115
|
// variables object literal.
|
|
6707
7116
|
const rawBodyWithFacetSplices = parsedBody !== null
|
|
6708
|
-
? applyFacetSplicePayloadSubstitutions(rawBodyWithFormSubs, parsedBody, restPayloadFieldNames, restOptionalFieldNames)
|
|
7117
|
+
? applyFacetSplicePayloadSubstitutions(rawBodyWithFormSubs, parsedBody, restPayloadFieldNames, restOptionalFieldNames, navigateToFacetOrder)
|
|
6709
7118
|
: rawBodyWithFormSubs;
|
|
6710
7119
|
// Optional-facet array-element splice — a navigateTo facet literal
|
|
6711
7120
|
// recurring as one element of an array field (not the `key:value`
|
|
@@ -6713,10 +7122,13 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6713
7122
|
// when its binding is optional and the caller omits it. Runs BEFORE
|
|
6714
7123
|
// Mechanism B (below) can swallow the whole array wholesale as an
|
|
6715
7124
|
// unparameterized structured value — see
|
|
6716
|
-
// {@link
|
|
7125
|
+
// {@link applyArrayFacetSplicePayloadSubstitutions}.
|
|
6717
7126
|
const rawBodyWithOptionalArrayFacetSplices = parsedBody !== null
|
|
6718
|
-
?
|
|
7127
|
+
? applyArrayFacetSplicePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, navigateToFacetOrder, facetArrayPathKeys, absorbedSiblingBodiesByStep.get(i))
|
|
6719
7128
|
: rawBodyWithFacetSplices;
|
|
7129
|
+
const rawBodyWithScalarFacetSplices = parsedBody !== null
|
|
7130
|
+
? applyScalarFacetSplicePayloadSubstitutions(rawBodyWithOptionalArrayFacetSplices, parsedBody, navigateToFacetOrder)
|
|
7131
|
+
: rawBodyWithOptionalArrayFacetSplices;
|
|
6720
7132
|
// Mechanism B — parameterize whole nested caller structures
|
|
6721
7133
|
// (experienceData/educationData history, opaque eventData) BEFORE value
|
|
6722
7134
|
// substitution reaches inside them: swallowing the entire array/object first
|
|
@@ -6727,7 +7139,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6727
7139
|
// dependent-drill-down join field (e.g. a bulk `{"tokens":[...]}` lookup)
|
|
6728
7140
|
// must stay reachable for state threading, not get frozen as caller data.
|
|
6729
7141
|
const rawBodyWithStructuredSubs = parsedBody !== null
|
|
6730
|
-
? applyStructuredValuePayloadSubstitutions(
|
|
7142
|
+
? applyStructuredValuePayloadSubstitutions(rawBodyWithScalarFacetSplices, parsedBody, outStructuredKeys, deriveStateVarByValue(prior, cap), joinFieldValuesByStep.get(i) ?? new Set(), payloadAccessorByValue, facetArrayPathKeys)
|
|
6731
7143
|
: rawBodyWithFormSubs;
|
|
6732
7144
|
// Whole-value caller coordinates bind here — after structured subs, BEFORE
|
|
6733
7145
|
// state threading — so a composite coordinate (a jobLocation or jobSeqNo
|
|
@@ -6759,7 +7171,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6759
7171
|
? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
|
|
6760
7172
|
: rawBodyWithProducerBoundary;
|
|
6761
7173
|
const bodyAfterStateAndKv = rawBodyWithUrlParams
|
|
6762
|
-
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues, joinFieldValuesByStep.get(i) ?? new Set()), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies)
|
|
7174
|
+
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues, joinFieldValuesByStep.get(i) ?? new Set()), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies, outStructuredKeys)
|
|
6763
7175
|
: "";
|
|
6764
7176
|
// Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
|
|
6765
7177
|
// rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
|
|
@@ -7650,6 +8062,87 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
|
|
|
7650
8062
|
}
|
|
7651
8063
|
return `[${elements.join(", ")}].join("")`;
|
|
7652
8064
|
}
|
|
8065
|
+
/**
|
|
8066
|
+
* Token-boundary match of one array element against the declared navigateTo
|
|
8067
|
+
* facets — the single place elements are matched so the GraphQL and REST
|
|
8068
|
+
* paths (and the REST accessor registration) never disagree on what a
|
|
8069
|
+
* facet-bearing element is.
|
|
8070
|
+
*/
|
|
8071
|
+
function matchArrayElementFacet(element, facets) {
|
|
8072
|
+
return matchFacetRecurrence(element, facets, { allowDelimiterSuffix: true });
|
|
8073
|
+
}
|
|
8074
|
+
/**
|
|
8075
|
+
* The one facet-recurrence predicate every grammar (array element, scalar
|
|
8076
|
+
* leaf) consults, so a declared facet literal is judged to recur identically
|
|
8077
|
+
* everywhere. A number leaf is compared by its string form, so a numeric body
|
|
8078
|
+
* value matches the string literal a navigateTo hash yields. With
|
|
8079
|
+
* `allowDelimiterSuffix`, a literal followed by a non-alphanumeric delimiter
|
|
8080
|
+
* suffix also matches. The longest literal wins.
|
|
8081
|
+
*/
|
|
8082
|
+
function matchFacetRecurrence(text, facets, options) {
|
|
8083
|
+
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
8084
|
+
return facets.reduce((best, facet) => {
|
|
8085
|
+
const literal = facet.value;
|
|
8086
|
+
const matches = text === literal ||
|
|
8087
|
+
(options.allowDelimiterSuffix &&
|
|
8088
|
+
literal.length > 0 &&
|
|
8089
|
+
text.startsWith(literal) &&
|
|
8090
|
+
!isAlnum(text[literal.length]));
|
|
8091
|
+
if (!matches)
|
|
8092
|
+
return best;
|
|
8093
|
+
return best === undefined || literal.length > best.value.length ? facet : best;
|
|
8094
|
+
}, undefined);
|
|
8095
|
+
}
|
|
8096
|
+
/**
|
|
8097
|
+
* Binds a top-level GraphQL variable whose scalar value (or, for a string, any
|
|
8098
|
+
* `key:value` packed segment's value) recurs as a declared navigateTo facet
|
|
8099
|
+
* literal, via the shared {@link matchFacetRecurrence} predicate so GraphQL
|
|
8100
|
+
* judges recurrence exactly as the array and REST scalar passes do.
|
|
8101
|
+
*/
|
|
8102
|
+
function spliceFacetRecurrenceIntoScalarVariable(value, facets) {
|
|
8103
|
+
if (facets.length === 0)
|
|
8104
|
+
return null;
|
|
8105
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
8106
|
+
const facet = matchFacetRecurrence(String(value), facets, { allowDelimiterSuffix: false });
|
|
8107
|
+
if (facet !== undefined)
|
|
8108
|
+
return `payload.${facet.field}`;
|
|
8109
|
+
}
|
|
8110
|
+
if (typeof value !== "string")
|
|
8111
|
+
return null;
|
|
8112
|
+
const segments = value.split(/([|,;])/);
|
|
8113
|
+
let bound = false;
|
|
8114
|
+
let anyOptional = false;
|
|
8115
|
+
const units = [];
|
|
8116
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
8117
|
+
const segment = segments[index];
|
|
8118
|
+
const trailingDelimiter = segments[index + 1] ?? "";
|
|
8119
|
+
const colonIndex = segment.indexOf(":");
|
|
8120
|
+
const facet = colonIndex < 0
|
|
8121
|
+
? undefined
|
|
8122
|
+
: matchFacetRecurrence(segment.slice(colonIndex + 1), facets, {
|
|
8123
|
+
allowDelimiterSuffix: false,
|
|
8124
|
+
});
|
|
8125
|
+
if (facet === undefined) {
|
|
8126
|
+
units.push(`\`${escapeForTemplateLiteral(segment + trailingDelimiter)}\``);
|
|
8127
|
+
continue;
|
|
8128
|
+
}
|
|
8129
|
+
bound = true;
|
|
8130
|
+
const unit = `\`${escapeForTemplateLiteral(segment.slice(0, colonIndex + 1))}\${payload.${facet.field}}${escapeForTemplateLiteral(trailingDelimiter)}\``;
|
|
8131
|
+
// An absent optional facet drops its own segment (and delimiter) rather
|
|
8132
|
+
// than rendering the string `undefined` into the packed value.
|
|
8133
|
+
if (facet.optional) {
|
|
8134
|
+
anyOptional = true;
|
|
8135
|
+
units.push(`...(payload.${facet.field} ? [${unit}] : [])`);
|
|
8136
|
+
continue;
|
|
8137
|
+
}
|
|
8138
|
+
units.push(unit);
|
|
8139
|
+
}
|
|
8140
|
+
if (!bound)
|
|
8141
|
+
return null;
|
|
8142
|
+
if (anyOptional)
|
|
8143
|
+
return `[${units.join(", ")}].join("")`;
|
|
8144
|
+
return `\`${units.map((unit) => unit.slice(1, -1)).join("")}\``;
|
|
8145
|
+
}
|
|
7653
8146
|
/**
|
|
7654
8147
|
* Splices a captured array-valued GraphQL variable's elements against
|
|
7655
8148
|
* navigateTo facet bindings instead of the `key:value` segment grammar
|
|
@@ -7665,24 +8158,16 @@ function spliceFacetsIntoStringVariable(value, fields, optionalFields = new Set(
|
|
|
7665
8158
|
* {@link spliceFacetsIntoStringVariable} uses, so an absent optional facet
|
|
7666
8159
|
* drops its element rather than emitting `undefined`.
|
|
7667
8160
|
*/
|
|
7668
|
-
function spliceFacetsIntoArrayVariable(value, facets) {
|
|
8161
|
+
function spliceFacetsIntoArrayVariable(value, facets, renderElement = (element) => JSON.stringify(element)) {
|
|
7669
8162
|
if (!Array.isArray(value))
|
|
7670
8163
|
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
8164
|
let matchCount = 0;
|
|
7680
8165
|
const elements = value.map((element) => {
|
|
7681
8166
|
if (typeof element !== "string")
|
|
7682
|
-
return
|
|
7683
|
-
const matched =
|
|
8167
|
+
return renderElement(element);
|
|
8168
|
+
const matched = matchArrayElementFacet(element, facets);
|
|
7684
8169
|
if (!matched)
|
|
7685
|
-
return
|
|
8170
|
+
return renderElement(element);
|
|
7686
8171
|
matchCount++;
|
|
7687
8172
|
const suffix = element.slice(matched.value.length);
|
|
7688
8173
|
const elementLiteral = `\`\${payload.${matched.field}}${escapeForTemplateLiteral(suffix)}\``;
|
|
@@ -7714,18 +8199,26 @@ function renderGqlVariablesExpr(variables, payloadFieldNames, optionalFieldNames
|
|
|
7714
8199
|
if (variables === null || typeof variables !== "object" || Array.isArray(variables))
|
|
7715
8200
|
return "{}";
|
|
7716
8201
|
const fields = payloadFieldNames ? [...payloadFieldNames] : [];
|
|
7717
|
-
const
|
|
7718
|
-
const
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
8202
|
+
const renderValue = (value) => {
|
|
8203
|
+
const spliced = spliceFacetsIntoStringVariable(value, fields, optionalFieldNames) ??
|
|
8204
|
+
spliceFacetsIntoArrayVariable(value, navigateToFacets, renderValue) ??
|
|
8205
|
+
spliceFacetRecurrenceIntoScalarVariable(value, navigateToFacets);
|
|
8206
|
+
if (spliced !== null)
|
|
8207
|
+
return spliced;
|
|
8208
|
+
if (Array.isArray(value))
|
|
8209
|
+
return `[${value.map(renderValue).join(", ")}]`;
|
|
8210
|
+
if (value !== null && typeof value === "object")
|
|
8211
|
+
return renderObject(value);
|
|
8212
|
+
return JSON.stringify(value);
|
|
8213
|
+
};
|
|
8214
|
+
const renderObject = (obj) => {
|
|
8215
|
+
const entries = Object.entries(obj).map(([key, value]) => {
|
|
8216
|
+
const matchedField = fields.find((field) => field.toLowerCase() === key.toLowerCase());
|
|
8217
|
+
return `${isValidJsIdentifier(key) ? key : JSON.stringify(key)}: ${matchedField ? `payload.${matchedField}` : renderValue(value)}`;
|
|
8218
|
+
});
|
|
8219
|
+
return entries.length > 0 ? `{ ${entries.join(", ")} }` : "{}";
|
|
8220
|
+
};
|
|
8221
|
+
return renderObject(variables);
|
|
7729
8222
|
}
|
|
7730
8223
|
/** A count-style page-size key (e.g. `count`, `limit`, `first`) paired with a
|
|
7731
8224
|
* skip-style offset key (e.g. `skip`, `offset`) in the same object is the
|
|
@@ -8063,7 +8556,7 @@ function collectRequestBodyValuesByKey(capture, allCaptures) {
|
|
|
8063
8556
|
const otherBody = parsedRequestBodyFor(c);
|
|
8064
8557
|
if (otherBody === undefined)
|
|
8065
8558
|
return undefined;
|
|
8066
|
-
const otherValue =
|
|
8559
|
+
const otherValue = readComparableBodyLeaf(parsedBody, otherBody, path);
|
|
8067
8560
|
return otherValue === undefined ? undefined : String(otherValue);
|
|
8068
8561
|
}
|
|
8069
8562
|
catch {
|
|
@@ -8114,10 +8607,10 @@ function isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, sibl
|
|
|
8114
8607
|
const pairs = [];
|
|
8115
8608
|
let sawUnexplainableField = false;
|
|
8116
8609
|
for (const body of allBodies) {
|
|
8117
|
-
const fieldRaw =
|
|
8610
|
+
const fieldRaw = readComparableBodyLeaf(parsedBody, body, path);
|
|
8118
8611
|
if (fieldRaw === undefined)
|
|
8119
8612
|
continue;
|
|
8120
|
-
const siblingRaw =
|
|
8613
|
+
const siblingRaw = readComparableBodyLeaf(parsedBody, body, pathSegments);
|
|
8121
8614
|
if (siblingRaw === undefined) {
|
|
8122
8615
|
// This body has a value for the field we're trying to explain but no
|
|
8123
8616
|
// value for the candidate sibling — the sibling can't vouch for it,
|
|
@@ -8228,7 +8721,7 @@ function findFrozenVaryingDrillParams(capture, renderedText, allCaptures) {
|
|
|
8228
8721
|
continue;
|
|
8229
8722
|
const fieldPath = path.join(".");
|
|
8230
8723
|
const differing = otherBodies
|
|
8231
|
-
.map((otherBody) =>
|
|
8724
|
+
.map((otherBody) => readComparableBodyLeaf(parsedBody, otherBody, path))
|
|
8232
8725
|
.find((v) => v !== undefined && String(v) !== stringValue);
|
|
8233
8726
|
if (differing !== undefined &&
|
|
8234
8727
|
!isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, new Set(allPaths.filter((p) => p !== fieldPath)))) {
|
|
@@ -9206,6 +9699,64 @@ function readValueAtPath(body, path) {
|
|
|
9206
9699
|
return node[segment];
|
|
9207
9700
|
}, body);
|
|
9208
9701
|
}
|
|
9702
|
+
const ARRAY_ELEMENT_IDENTITY = /^[\s\S]*([^A-Za-z0-9=]\w+=[\s\S]*)$/;
|
|
9703
|
+
/** The `<delimiter><name>=<anything>` suffix of a self-describing array element
|
|
9704
|
+
* (the grammar {@link matchArrayElementFacet} binds on), or `undefined`. The
|
|
9705
|
+
* suffix is open-ended because slugs and names carry hyphens, dots and colons
|
|
9706
|
+
* — a stricter shape would silently fall back to index comparison for them. */
|
|
9707
|
+
function arrayElementIdentity(element) {
|
|
9708
|
+
if (typeof element !== "string")
|
|
9709
|
+
return undefined;
|
|
9710
|
+
return ARRAY_ELEMENT_IDENTITY.exec(element)?.[1];
|
|
9711
|
+
}
|
|
9712
|
+
/**
|
|
9713
|
+
* Index of the innermost path segment that indexes into an array (the array
|
|
9714
|
+
* holding the leaf, or one holding an object element that carries it), or -1.
|
|
9715
|
+
*/
|
|
9716
|
+
function innermostArrayIndexSegment(body, path) {
|
|
9717
|
+
for (let index = path.length - 1; index >= 0; index--) {
|
|
9718
|
+
if (!/^\d+$/.test(path[index]))
|
|
9719
|
+
continue;
|
|
9720
|
+
if (Array.isArray(readValueAtPath(body, path.slice(0, index))))
|
|
9721
|
+
return index;
|
|
9722
|
+
}
|
|
9723
|
+
return -1;
|
|
9724
|
+
}
|
|
9725
|
+
/**
|
|
9726
|
+
* Counterpart of the primitive leaf at `path` in `body`, read from `otherBody`.
|
|
9727
|
+
* A string leaf inside an array of self-describing elements (directly, or as a
|
|
9728
|
+
* field of an object element) is matched by its identity suffix within the same
|
|
9729
|
+
* array, never by index, so differently shaped arrays are not mistaken for one
|
|
9730
|
+
* array with a varying slot; an identity the other body lacks yields
|
|
9731
|
+
* `undefined`. A plain element of such an array is matched by value, since its
|
|
9732
|
+
* slot says nothing about it once the arrays' shapes differ. Every other path
|
|
9733
|
+
* defers to {@link readValueAtPath}.
|
|
9734
|
+
*/
|
|
9735
|
+
function readComparableBodyLeaf(body, otherBody, path) {
|
|
9736
|
+
const leaf = readValueAtPath(body, path);
|
|
9737
|
+
const arrayEnd = typeof leaf === "string" ? innermostArrayIndexSegment(body, path) : -1;
|
|
9738
|
+
if (arrayEnd === -1)
|
|
9739
|
+
return readValueAtPath(otherBody, path);
|
|
9740
|
+
const arrayPath = path.slice(0, arrayEnd);
|
|
9741
|
+
const elementSubpath = path.slice(arrayEnd + 1);
|
|
9742
|
+
const ownArray = readValueAtPath(body, arrayPath);
|
|
9743
|
+
const otherArray = readValueAtPath(otherBody, arrayPath);
|
|
9744
|
+
const identity = arrayElementIdentity(leaf);
|
|
9745
|
+
const isSelfDescribing = (array) => array.some((element) => arrayElementIdentity(readValueAtPath(element, elementSubpath)));
|
|
9746
|
+
if (identity === undefined &&
|
|
9747
|
+
!isSelfDescribing(ownArray) &&
|
|
9748
|
+
!(Array.isArray(otherArray) && isSelfDescribing(otherArray))) {
|
|
9749
|
+
return readValueAtPath(otherBody, path);
|
|
9750
|
+
}
|
|
9751
|
+
if (!Array.isArray(otherArray))
|
|
9752
|
+
return undefined;
|
|
9753
|
+
const counterparts = otherArray.map((element) => readValueAtPath(element, elementSubpath));
|
|
9754
|
+
if (counterparts.includes(leaf))
|
|
9755
|
+
return leaf;
|
|
9756
|
+
if (identity === undefined)
|
|
9757
|
+
return undefined;
|
|
9758
|
+
return counterparts.find((candidate) => arrayElementIdentity(candidate) === identity);
|
|
9759
|
+
}
|
|
9209
9760
|
/**
|
|
9210
9761
|
* Turns a flow author's plain dot-separated {@link FoldReturnSpec.resultsPath}
|
|
9211
9762
|
* / `drillResultsPath` into the SAME {@link ARRAY_WILDCARD_SEGMENT}-bearing
|
|
@@ -10730,6 +11281,64 @@ function buildContractChecklist(opts) {
|
|
|
10730
11281
|
outOfTreeChecklistLine,
|
|
10731
11282
|
].filter((line) => line !== "");
|
|
10732
11283
|
}
|
|
11284
|
+
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;
|
|
11285
|
+
const PAYLOAD_CHAIN_SEGMENT = /\??\.([A-Za-z_$][A-Za-z0-9_$]*)|\[(?:"[^"\]]*"|\d+)\]/g;
|
|
11286
|
+
function newAccessorShape() {
|
|
11287
|
+
return { isArray: false, element: null, keys: new Map() };
|
|
11288
|
+
}
|
|
11289
|
+
function renderAccessorShape(shape) {
|
|
11290
|
+
if (shape.isArray) {
|
|
11291
|
+
return `z.array(${shape.element ? renderAccessorShape(shape.element) : "z.string()"})`;
|
|
11292
|
+
}
|
|
11293
|
+
if (shape.keys.size === 0)
|
|
11294
|
+
return "z.string()";
|
|
11295
|
+
const entries = [...shape.keys.entries()]
|
|
11296
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
11297
|
+
.map(([key, child]) => `${isValidJsIdentifier(key) ? key : JSON.stringify(key)}: ${renderAccessorShape(child)}`);
|
|
11298
|
+
return `z.object({ ${entries.join(", ")} })`;
|
|
11299
|
+
}
|
|
11300
|
+
/**
|
|
11301
|
+
* Derives each `payload.<field>` Zod type from the full accessor chain the
|
|
11302
|
+
* emitted body text uses (`payload.f["0"]!.x` => array of objects with key
|
|
11303
|
+
* `x`), so the schema can never reject a property access the body performs.
|
|
11304
|
+
*/
|
|
11305
|
+
function deriveBodyAccessorShapes(body) {
|
|
11306
|
+
const roots = new Map();
|
|
11307
|
+
for (const m of body.matchAll(PAYLOAD_ACCESSOR_CHAIN)) {
|
|
11308
|
+
const root = roots.get(m[1]) ?? newAccessorShape();
|
|
11309
|
+
roots.set(m[1], root);
|
|
11310
|
+
let node = root;
|
|
11311
|
+
for (const seg of m[2].matchAll(PAYLOAD_CHAIN_SEGMENT)) {
|
|
11312
|
+
if (seg[1] === undefined) {
|
|
11313
|
+
node.isArray = true;
|
|
11314
|
+
node.element ??= newAccessorShape();
|
|
11315
|
+
node = node.element;
|
|
11316
|
+
continue;
|
|
11317
|
+
}
|
|
11318
|
+
const child = node.keys.get(seg[1]) ?? newAccessorShape();
|
|
11319
|
+
node.keys.set(seg[1], child);
|
|
11320
|
+
node = child;
|
|
11321
|
+
}
|
|
11322
|
+
}
|
|
11323
|
+
return roots;
|
|
11324
|
+
}
|
|
11325
|
+
function isStructuredAccessorShape(shape) {
|
|
11326
|
+
return shape.isArray || shape.keys.size > 0;
|
|
11327
|
+
}
|
|
11328
|
+
/**
|
|
11329
|
+
* Whether a declared Zod expression already admits every access the shape
|
|
11330
|
+
* demands (array-ness and each dereferenced key), so a richer structured
|
|
11331
|
+
* declaration is kept instead of being narrowed to the accessor-derived one.
|
|
11332
|
+
*/
|
|
11333
|
+
function declaredSchemaCoversAccessorShape(declared, shape) {
|
|
11334
|
+
if (shape.isArray && !/\bz\.array\(/.test(declared))
|
|
11335
|
+
return false;
|
|
11336
|
+
if (!shape.isArray && shape.keys.size > 0 && !/\bz\.(object|record)\(/.test(declared)) {
|
|
11337
|
+
return false;
|
|
11338
|
+
}
|
|
11339
|
+
const childShapes = [...(shape.element ? [shape.element] : []), ...shape.keys.values()];
|
|
11340
|
+
return ([...shape.keys.keys()].every((key) => new RegExp(`(?:^|[\\s{,])"?${key.replace(/[$]/g, "\\$&")}"?\\s*:`).test(declared)) && childShapes.every((child) => declaredSchemaCoversAccessorShape(declared, child)));
|
|
11341
|
+
}
|
|
10733
11342
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
10734
11343
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
10735
11344
|
function emitContractTs(opts) {
|
|
@@ -10891,7 +11500,10 @@ function emitContractTs(opts) {
|
|
|
10891
11500
|
// override semantics a chain of `.extend()` calls used to have (each
|
|
10892
11501
|
// subsequent `.extend` replaced an earlier field of the same name).
|
|
10893
11502
|
const extendFields = new Map();
|
|
11503
|
+
const structuredFieldNames = new Set();
|
|
10894
11504
|
const addExtendField = (name, line) => {
|
|
11505
|
+
if (structuredFieldNames.has(name))
|
|
11506
|
+
return;
|
|
10895
11507
|
extendFields.set(name, line);
|
|
10896
11508
|
};
|
|
10897
11509
|
// A detected bounded-paging signal means buildPaginatedFetchLoopExecuteHttpBody
|
|
@@ -11049,7 +11661,8 @@ function emitContractTs(opts) {
|
|
|
11049
11661
|
continue;
|
|
11050
11662
|
const key = isValidJsIdentifier(name) ? name : JSON.stringify(name);
|
|
11051
11663
|
const value = payloadNeedsMultipart ? `multipartJsonObject(${schema})` : schema;
|
|
11052
|
-
|
|
11664
|
+
extendFields.set(name, ` ${key}: ${value},`);
|
|
11665
|
+
structuredFieldNames.add(name);
|
|
11053
11666
|
}
|
|
11054
11667
|
// Closing-the-loop safety net: every discovered-field source above tracks
|
|
11055
11668
|
// its own registration as it splices a `payload.<field>` accessor into the
|
|
@@ -11068,14 +11681,34 @@ function emitContractTs(opts) {
|
|
|
11068
11681
|
// its structural root regardless of which upstream pass forgot to record a
|
|
11069
11682
|
// field, instead of adding a fifth registration site that could itself be
|
|
11070
11683
|
// forgotten by a future pass.
|
|
11684
|
+
const accessorStructuredFields = new Set();
|
|
11071
11685
|
if (multiStepBody) {
|
|
11072
|
-
const bodyReferencedFields =
|
|
11073
|
-
for (const name of [...bodyReferencedFields].sort()) {
|
|
11074
|
-
if (extendFields.has(name))
|
|
11075
|
-
continue;
|
|
11686
|
+
const bodyReferencedFields = deriveBodyAccessorShapes(multiStepBody);
|
|
11687
|
+
for (const name of [...bodyReferencedFields.keys()].sort()) {
|
|
11076
11688
|
if (isReservedByApplicantContactSchema(name))
|
|
11077
11689
|
continue;
|
|
11078
|
-
|
|
11690
|
+
const shape = bodyReferencedFields.get(name);
|
|
11691
|
+
const structured = isStructuredAccessorShape(shape);
|
|
11692
|
+
// The body text is the ground truth for a field's shape: a scalar
|
|
11693
|
+
// registration from any source must not outlive an accessor that indexes
|
|
11694
|
+
// or dereferences the field, and a structured declaration is kept only
|
|
11695
|
+
// when it already admits every access the body performs.
|
|
11696
|
+
if (structured) {
|
|
11697
|
+
const declared = extendFields.get(name);
|
|
11698
|
+
if (structuredFieldNames.has(name) &&
|
|
11699
|
+
declared !== undefined &&
|
|
11700
|
+
declaredSchemaCoversAccessorShape(declared, shape)) {
|
|
11701
|
+
accessorStructuredFields.add(name);
|
|
11702
|
+
continue;
|
|
11703
|
+
}
|
|
11704
|
+
accessorStructuredFields.add(name);
|
|
11705
|
+
structuredFieldNames.delete(name);
|
|
11706
|
+
}
|
|
11707
|
+
else if (extendFields.has(name)) {
|
|
11708
|
+
continue;
|
|
11709
|
+
}
|
|
11710
|
+
const zod = renderAccessorShape(shape);
|
|
11711
|
+
addExtendField(name, ` ${name}: ${zod === "z.string()" || !payloadNeedsMultipart ? zod : `multipartJsonObject(${zod})`},`);
|
|
11079
11712
|
}
|
|
11080
11713
|
}
|
|
11081
11714
|
// The structural walk over the captured request body that used to BE the
|
|
@@ -11123,6 +11756,8 @@ function emitContractTs(opts) {
|
|
|
11123
11756
|
// capture this run and is a no-op, not an error: there is no schema line
|
|
11124
11757
|
// for it to attach to.
|
|
11125
11758
|
for (const [fieldName, constraint] of Object.entries(valueConstraints)) {
|
|
11759
|
+
if (accessorStructuredFields.has(fieldName))
|
|
11760
|
+
continue;
|
|
11126
11761
|
const line = extendFields.get(fieldName);
|
|
11127
11762
|
if (line === null || line === undefined)
|
|
11128
11763
|
continue;
|
|
@@ -11243,7 +11878,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
11243
11878
|
// the same captured request-variables object the signal was itself
|
|
11244
11879
|
// detected from (see paginationOperationIdentity above) — the default
|
|
11245
11880
|
// `{ q: payload.query }` REST body has no skip/count container to advance.
|
|
11246
|
-
const navigateToFacets = extractNavigateToFacetOrder(contractFlowSteps);
|
|
11881
|
+
const navigateToFacets = resolveRecurringNavigateToFacets(extractNavigateToFacetOrder(contractFlowSteps), actionSteps);
|
|
11247
11882
|
const gqlVariablesExpr = gqlOperationName
|
|
11248
11883
|
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
|
|
11249
11884
|
: paginationSignal
|