@enricai/barnacle 1.12.74 → 1.12.76
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.map +1 -1
- package/dist/scraper/flow-runner.js +5 -2
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +20 -0
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +36 -2
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +9 -5
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +16 -10
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +94 -1
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +323 -23
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,8 +24,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
24
24
|
exports.resolveStepPayloadField = resolveStepPayloadField;
|
|
25
25
|
exports.buildKnownFieldValues = buildKnownFieldValues;
|
|
26
26
|
exports.extractStepPersonaValue = extractStepPersonaValue;
|
|
27
|
+
exports.extractNavigateToHashFragmentValue = extractNavigateToHashFragmentValue;
|
|
27
28
|
exports.deriveFillLabelField = deriveFillLabelField;
|
|
28
29
|
exports.harvestPersonaBindings = harvestPersonaBindings;
|
|
30
|
+
exports.extractNavigateToFacetOrder = extractNavigateToFacetOrder;
|
|
29
31
|
exports.resolveCompositePersonaFields = resolveCompositePersonaFields;
|
|
30
32
|
exports.collectConditionalGraphQLFieldNames = collectConditionalGraphQLFieldNames;
|
|
31
33
|
exports.inferZodSchemaFromSamples = inferZodSchemaFromSamples;
|
|
@@ -60,6 +62,7 @@ exports.ancestorScopedAccessor = ancestorScopedAccessor;
|
|
|
60
62
|
exports.compileActionSteps = compileActionSteps;
|
|
61
63
|
exports.collectHeaderBindings = collectHeaderBindings;
|
|
62
64
|
exports.deriveProducerBoundaryBindings = deriveProducerBoundaryBindings;
|
|
65
|
+
exports.collectUnboundLiterals = collectUnboundLiterals;
|
|
63
66
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
64
67
|
exports.countSpliceableFacets = countSpliceableFacets;
|
|
65
68
|
exports.spliceFacetsIntoStringVariable = spliceFacetsIntoStringVariable;
|
|
@@ -359,6 +362,34 @@ function extractStepPersonaValue(instruction, env) {
|
|
|
359
362
|
const value = pickValueSpan(instruction, spans).value;
|
|
360
363
|
return value.length > 0 ? value : null;
|
|
361
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Extracts the trailing path segment of a navigateTo URL's hash fragment —
|
|
367
|
+
* e.g. `.../#/jobs/category-widgets` yields `widgets` — as the persona value
|
|
368
|
+
* an explicit `payloadField` annotation on that step names. Site-agnostic:
|
|
369
|
+
* reads only the URL's own hash shape, no domain knowledge.
|
|
370
|
+
*
|
|
371
|
+
* When `previousHash` is given and the current hash is a strict extension of
|
|
372
|
+
* it (the accumulating-hash case, e.g. `#a` -> `#a,b`), the persona value is
|
|
373
|
+
* only the NEW suffix the step added, not the whole cumulative string — a
|
|
374
|
+
* cumulative value would never recur verbatim in any single request literal.
|
|
375
|
+
*
|
|
376
|
+
* @returns the segment (or delta), or null when the URL has no non-empty
|
|
377
|
+
* hash segment
|
|
378
|
+
*/
|
|
379
|
+
function extractNavigateToHashFragmentValue(url, previousHash) {
|
|
380
|
+
const hashIndex = url.indexOf("#");
|
|
381
|
+
if (hashIndex === -1)
|
|
382
|
+
return null;
|
|
383
|
+
const hash = url.slice(hashIndex + 1);
|
|
384
|
+
if (previousHash && hash.length > previousHash.length && hash.startsWith(previousHash)) {
|
|
385
|
+
const suffix = hash.slice(previousHash.length);
|
|
386
|
+
const delta = /^[,/]/.test(suffix) ? suffix.slice(1) : suffix;
|
|
387
|
+
if (delta.length > 0)
|
|
388
|
+
return delta;
|
|
389
|
+
}
|
|
390
|
+
const segments = hash.split("/").filter((s) => s.length > 0);
|
|
391
|
+
return segments.length > 0 ? segments[segments.length - 1] : null;
|
|
392
|
+
}
|
|
362
393
|
/**
|
|
363
394
|
* Derives a payload field name from the field LABEL in a fill/enter/type
|
|
364
395
|
* instruction, for steps the consumer vocabulary does not cover.
|
|
@@ -405,9 +436,32 @@ function deriveFillLabelField(instruction) {
|
|
|
405
436
|
function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
406
437
|
const bindings = new Map();
|
|
407
438
|
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
439
|
+
let previousNavigateToHash;
|
|
408
440
|
for (const step of flowSteps) {
|
|
409
441
|
const isObj = typeof step !== "string";
|
|
410
442
|
const instruction = isObj ? step.step : step;
|
|
443
|
+
// A navigateTo step carries no prose instruction to extract a persona
|
|
444
|
+
// value FROM — its own text is a captured page.goto, not a fill/select
|
|
445
|
+
// label. The facet an explicit `payloadField` annotation names instead
|
|
446
|
+
// lives in the URL itself (recon-browser captures navigation targets like
|
|
447
|
+
// `.../#/jobs/category-widgets`), so the value to correlate against
|
|
448
|
+
// downstream header/body templates is the URL's own hash-derived
|
|
449
|
+
// 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}`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
411
465
|
const vocabField = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
412
466
|
// Vocabulary wins outright. Only on a miss do we fall back to deriving the
|
|
413
467
|
// field from the instruction's own label — and never when the author opted
|
|
@@ -428,6 +482,36 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
428
482
|
}
|
|
429
483
|
return bindings;
|
|
430
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Re-walks the flow for navigateTo+payloadField steps in isolation, in flow
|
|
487
|
+
* order, independent of whether {@link harvestPersonaBindings} could anchor
|
|
488
|
+
* each one's literal anywhere in the capture. `harvestPersonaBindings` only
|
|
489
|
+
* sees flow steps (not captures), so it cannot tell which of its bindings
|
|
490
|
+
* will turn out to have zero recurrence downstream — this lets the emitter,
|
|
491
|
+
* which DOES see the captures, re-derive the same (value, field) pairs and
|
|
492
|
+
* try the causally-adjacent-action fallback (see `emitMultiStepExecuteHttp`)
|
|
493
|
+
* for whichever ones the recurrence-anchored path left unbound.
|
|
494
|
+
*/
|
|
495
|
+
function extractNavigateToFacetOrder(flowSteps) {
|
|
496
|
+
const order = [];
|
|
497
|
+
let previousNavigateToHash;
|
|
498
|
+
for (const step of flowSteps) {
|
|
499
|
+
if (typeof step === "string" || step.navigateTo === undefined)
|
|
500
|
+
continue;
|
|
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;
|
|
514
|
+
}
|
|
431
515
|
/**
|
|
432
516
|
* Detects a flow step whose quoted VALUE is the space-joined concatenation of
|
|
433
517
|
* two already-known field values (either order), e.g. a signature step whose
|
|
@@ -3913,7 +3997,7 @@ function locateFormEnvelopePath(parsedBody) {
|
|
|
3913
3997
|
* search there stops an earlier element's already-rewritten (or still-frozen)
|
|
3914
3998
|
* span from swallowing a later element's identically-named key.
|
|
3915
3999
|
*/
|
|
3916
|
-
function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys,
|
|
4000
|
+
function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, rootBody) {
|
|
3917
4001
|
let result = template;
|
|
3918
4002
|
let cursor = searchFrom;
|
|
3919
4003
|
// The body root's own entries are eligible too, whenever the located
|
|
@@ -3935,8 +4019,29 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
3935
4019
|
Object.keys(value).length > 0;
|
|
3936
4020
|
if (!isNonEmptyArray && !isNestedObject)
|
|
3937
4021
|
continue;
|
|
3938
|
-
if (
|
|
3939
|
-
|
|
4022
|
+
if (unconditionalExcludeValues.size > 0 || restrictedExcludeSourceByValue.size > 0) {
|
|
4023
|
+
// A name-free join/chain value (unconditionalExcludeValues) excludes the
|
|
4024
|
+
// whole field on ANY leaf coincidence, same as before. A RESTRICTED prior-
|
|
4025
|
+
// step value (one with a real source field name) additionally requires
|
|
4026
|
+
// keyNamesCorrelate(sourceName, key) — mirroring the discipline
|
|
4027
|
+
// replaceGuardedAgainstExistingPlaceholders/findThreadedJoinFields already
|
|
4028
|
+
// apply to the exact same class of value-coincidence false positive.
|
|
4029
|
+
// Without this, a broken field's own unrelated short scalar leaf (a
|
|
4030
|
+
// count/id) merely equalling SOME other step's produced value gets the
|
|
4031
|
+
// ENTIRE array/object excluded from threading, even though nothing about
|
|
4032
|
+
// that leaf's key names the same concept as the matched value's source.
|
|
4033
|
+
const carriesThreadedValue = [...walkAllPrimitiveLeaves(value)].some(({ value: leaf, path: leafPath }) => {
|
|
4034
|
+
if (leaf === null)
|
|
4035
|
+
return false;
|
|
4036
|
+
const leafValue = String(leaf);
|
|
4037
|
+
if (unconditionalExcludeValues.has(leafValue))
|
|
4038
|
+
return true;
|
|
4039
|
+
const sourceName = restrictedExcludeSourceByValue.get(leafValue);
|
|
4040
|
+
if (sourceName === undefined)
|
|
4041
|
+
return false;
|
|
4042
|
+
const leafKey = leafPath.at(-1);
|
|
4043
|
+
return leafKey !== undefined && keyNamesCorrelate(sourceName, leafKey);
|
|
4044
|
+
});
|
|
3940
4045
|
if (carriesThreadedValue)
|
|
3941
4046
|
continue;
|
|
3942
4047
|
}
|
|
@@ -3993,7 +4098,7 @@ function applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope,
|
|
|
3993
4098
|
* pass to it. Shared by the top-level-object and top-level-array branches of
|
|
3994
4099
|
* {@link applyStructuredValuePayloadSubstitutions} below.
|
|
3995
4100
|
*/
|
|
3996
|
-
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys,
|
|
4101
|
+
function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom) {
|
|
3997
4102
|
const envelopePath = locateFormEnvelopePath(objectBody);
|
|
3998
4103
|
let envelope = objectBody;
|
|
3999
4104
|
for (const seg of envelopePath) {
|
|
@@ -4004,12 +4109,33 @@ function applyStructuredValuePayloadSubstitutionsForObjectBody(template, objectB
|
|
|
4004
4109
|
if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
4005
4110
|
return { result: template, nextSearchFrom: searchFrom };
|
|
4006
4111
|
}
|
|
4007
|
-
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys,
|
|
4112
|
+
return applyStructuredValuePayloadSubstitutionsForEnvelope(template, envelope, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, searchFrom, envelopePath, objectBody);
|
|
4008
4113
|
}
|
|
4009
|
-
|
|
4114
|
+
/**
|
|
4115
|
+
* `priorStepStateBindings` carries the SAME value→binding map
|
|
4116
|
+
* {@link deriveStateVarByValue} produces. A binding with `restricted: true`
|
|
4117
|
+
* (a value whose source has a real field name) only excludes a candidate
|
|
4118
|
+
* field when its own leaf's key {@link keyNamesCorrelate}s with that source
|
|
4119
|
+
* name — an unrestricted binding (name-free, e.g. a bare array-index chain
|
|
4120
|
+
* hop) or a `joinFieldValues` entry excludes unconditionally on any leaf
|
|
4121
|
+
* match, same as before. Without the correlation requirement, an array/
|
|
4122
|
+
* object field whose own unrelated short scalar leaf (a count/id) merely
|
|
4123
|
+
* EQUALS some other step's produced value gets the whole field wrongly
|
|
4124
|
+
* excluded from threading — the same value-coincidence class of bug
|
|
4125
|
+
* {@link keyNamesCorrelate}'s other call sites already guard against.
|
|
4126
|
+
*/
|
|
4127
|
+
function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStructuredKeys, priorStepStateBindings = new Map(), joinFieldValues = new Set()) {
|
|
4010
4128
|
if (parsedBody === null || typeof parsedBody !== "object") {
|
|
4011
4129
|
return template;
|
|
4012
4130
|
}
|
|
4131
|
+
const unconditionalExcludeValues = new Set(joinFieldValues);
|
|
4132
|
+
const restrictedExcludeSourceByValue = new Map();
|
|
4133
|
+
for (const [value, binding] of priorStepStateBindings) {
|
|
4134
|
+
if (binding.restricted)
|
|
4135
|
+
restrictedExcludeSourceByValue.set(value, binding.sourceName);
|
|
4136
|
+
else
|
|
4137
|
+
unconditionalExcludeValues.add(value);
|
|
4138
|
+
}
|
|
4013
4139
|
// A cruise-line-style multi-room/multi-guest search body commonly batches
|
|
4014
4140
|
// per-element criteria as a top-level JSON ARRAY rather than a single
|
|
4015
4141
|
// object. Walk each element in textual order (via the growing searchFrom
|
|
@@ -4022,13 +4148,13 @@ function applyStructuredValuePayloadSubstitutions(template, parsedBody, outStruc
|
|
|
4022
4148
|
for (const element of parsedBody) {
|
|
4023
4149
|
if (element === null || typeof element !== "object" || Array.isArray(element))
|
|
4024
4150
|
continue;
|
|
4025
|
-
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys,
|
|
4151
|
+
const { result: nextResult, nextSearchFrom } = applyStructuredValuePayloadSubstitutionsForObjectBody(result, element, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, cursor);
|
|
4026
4152
|
result = nextResult;
|
|
4027
4153
|
cursor = nextSearchFrom;
|
|
4028
4154
|
}
|
|
4029
4155
|
return result;
|
|
4030
4156
|
}
|
|
4031
|
-
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys,
|
|
4157
|
+
const { result } = applyStructuredValuePayloadSubstitutionsForObjectBody(template, parsedBody, outStructuredKeys, unconditionalExcludeValues, restrictedExcludeSourceByValue, 0);
|
|
4032
4158
|
return result;
|
|
4033
4159
|
}
|
|
4034
4160
|
/** Maximum length to guard against indexing massive blobs (HTML fragments,
|
|
@@ -5742,13 +5868,14 @@ function applyVolatileFieldSubstitutions(template, parsedBody, shieldedUuids, bo
|
|
|
5742
5868
|
return result;
|
|
5743
5869
|
}
|
|
5744
5870
|
/**
|
|
5745
|
-
* Collects captured string leaves
|
|
5746
|
-
* still-literal — the values a reviewer must
|
|
5747
|
-
* traced to a payload field, a generator, or
|
|
5748
|
-
* key names (deduped, in first-seen order)
|
|
5749
|
-
* `// TODO: unbound captured literal`
|
|
5750
|
-
* file still compiles. Short
|
|
5751
|
-
* skipped — they are the
|
|
5871
|
+
* Collects captured string leaves AND whole array/object values that survived
|
|
5872
|
+
* every binding/generation pass as still-literal — the values a reviewer must
|
|
5873
|
+
* look at because they couldn't be traced to a payload field, a generator, or
|
|
5874
|
+
* a schema anchor. Returns the JSON key names (deduped, in first-seen order)
|
|
5875
|
+
* so the emitter can prepend a single `// TODO: unbound captured literal`
|
|
5876
|
+
* marker; it never mutates the body, so the file still compiles. Short string
|
|
5877
|
+
* values (< {@link MIN_STATE_VALUE_LENGTH}) are skipped — they are the
|
|
5878
|
+
* legitimately-constant enum-like fields.
|
|
5752
5879
|
*/
|
|
5753
5880
|
function collectUnboundLiterals(finalTemplate, parsedBody, shieldedUuids) {
|
|
5754
5881
|
const unbound = [];
|
|
@@ -5767,6 +5894,20 @@ function collectUnboundLiterals(finalTemplate, parsedBody, shieldedUuids) {
|
|
|
5767
5894
|
unbound.push(key);
|
|
5768
5895
|
}
|
|
5769
5896
|
}
|
|
5897
|
+
if (parsedBody !== null && typeof parsedBody === "object") {
|
|
5898
|
+
for (const [key, value] of Object.entries(parsedBody)) {
|
|
5899
|
+
if (value === null || typeof value !== "object")
|
|
5900
|
+
continue;
|
|
5901
|
+
if (seen.has(key))
|
|
5902
|
+
continue;
|
|
5903
|
+
// Whole array/object value never got threaded as a unit — ${JSON.stringify(...)}
|
|
5904
|
+
// never replaced it, so it still sits verbatim in the emitted template.
|
|
5905
|
+
if (finalTemplate.includes(JSON.stringify(value))) {
|
|
5906
|
+
seen.add(key);
|
|
5907
|
+
unbound.push(key);
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5770
5911
|
return unbound;
|
|
5771
5912
|
}
|
|
5772
5913
|
/** Builds the multi-step `executeHttp` body as a single template-literal string.
|
|
@@ -5902,6 +6043,109 @@ function emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinA
|
|
|
5902
6043
|
` Object.assign(${itemVar}, Object.fromEntries(Object.entries(foldMatch${suffix} ?? {}).filter(([k]) => !(k in ${itemVar}))));`,
|
|
5903
6044
|
];
|
|
5904
6045
|
}
|
|
6046
|
+
/** Parses a URL's query string into a key→value map. Returns an empty map on
|
|
6047
|
+
* a malformed URL or when there is no query string. */
|
|
6048
|
+
function parseUrlQueryParams(url) {
|
|
6049
|
+
const params = new Map();
|
|
6050
|
+
try {
|
|
6051
|
+
for (const [key, value] of new URL(url).searchParams)
|
|
6052
|
+
params.set(key, value);
|
|
6053
|
+
}
|
|
6054
|
+
catch {
|
|
6055
|
+
// malformed URL — treat as having no query params
|
|
6056
|
+
}
|
|
6057
|
+
return params;
|
|
6058
|
+
}
|
|
6059
|
+
/** Parses a captured JSON request body into its string leaf values. Returns
|
|
6060
|
+
* an empty array when the body is absent or not valid JSON. */
|
|
6061
|
+
function parseBodyLeafValues(requestPostData) {
|
|
6062
|
+
if (!requestPostData)
|
|
6063
|
+
return [];
|
|
6064
|
+
try {
|
|
6065
|
+
return [...walkStringLeaves(JSON.parse(requestPostData))].map((leaf) => leaf.value);
|
|
6066
|
+
}
|
|
6067
|
+
catch {
|
|
6068
|
+
return [];
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
/**
|
|
6072
|
+
* Walks `actions` in order and groups the query-string/body leaf values that
|
|
6073
|
+
* appear in an action's own capture but did NOT appear in the immediately-
|
|
6074
|
+
* preceding action's capture, PER TRANSITION (one entry per adjacent action
|
|
6075
|
+
* pair that introduced at least one such value). This is the generic
|
|
6076
|
+
* "what's new here" diff {@link correlateUnreachableNavigateToFacets} uses —
|
|
6077
|
+
* kept per-transition (rather than flattened) so a transition that
|
|
6078
|
+
* introduces more than one new value, or multiple transitions that fire
|
|
6079
|
+
* between two facets' real actions, can be told apart from the single clean
|
|
6080
|
+
* transition a lone facet causes.
|
|
6081
|
+
*
|
|
6082
|
+
* The first action never contributes: with no predecessor to diff against,
|
|
6083
|
+
* every one of its values would trivially count as "new", which is nearly
|
|
6084
|
+
* always the baseline/listing call a navigateTo-declared filter does NOT
|
|
6085
|
+
* affect (the already-working recurrence-anchored path exists precisely
|
|
6086
|
+
* because a real facet DOES recur in a later request; nothing here changes
|
|
6087
|
+
* that — this only supplies values for facets that are not found at all).
|
|
6088
|
+
*/
|
|
6089
|
+
function collectNewlyAppearingRequestValueTransitions(actions) {
|
|
6090
|
+
const seenGlobally = new Set();
|
|
6091
|
+
const transitions = [];
|
|
6092
|
+
for (let i = 1; i < actions.length; i++) {
|
|
6093
|
+
const prior = actions[i - 1].capture;
|
|
6094
|
+
const current = actions[i].capture;
|
|
6095
|
+
const seenThisTransition = new Set();
|
|
6096
|
+
const values = [];
|
|
6097
|
+
const record = (value) => {
|
|
6098
|
+
if (value.length === 0 || seenGlobally.has(value) || seenThisTransition.has(value))
|
|
6099
|
+
return;
|
|
6100
|
+
seenThisTransition.add(value);
|
|
6101
|
+
values.push(value);
|
|
6102
|
+
};
|
|
6103
|
+
const priorParams = parseUrlQueryParams(prior.url);
|
|
6104
|
+
for (const [key, value] of parseUrlQueryParams(current.url)) {
|
|
6105
|
+
if (priorParams.get(key) !== value)
|
|
6106
|
+
record(value);
|
|
6107
|
+
}
|
|
6108
|
+
const priorLeaves = new Set(parseBodyLeafValues(prior.requestPostData));
|
|
6109
|
+
for (const value of parseBodyLeafValues(current.requestPostData)) {
|
|
6110
|
+
if (!priorLeaves.has(value))
|
|
6111
|
+
record(value);
|
|
6112
|
+
}
|
|
6113
|
+
for (const value of values)
|
|
6114
|
+
seenGlobally.add(value);
|
|
6115
|
+
if (values.length > 0)
|
|
6116
|
+
transitions.push(values);
|
|
6117
|
+
}
|
|
6118
|
+
return transitions;
|
|
6119
|
+
}
|
|
6120
|
+
/**
|
|
6121
|
+
* Resolves each zero-recurrence navigateTo facet (in flow order) to the real
|
|
6122
|
+
* request value it causally affects, or returns `null` when the correlation
|
|
6123
|
+
* cannot be trusted.
|
|
6124
|
+
*
|
|
6125
|
+
* Pairing is positional — facet N gets the Nth qualifying transition — which
|
|
6126
|
+
* is only safe when every transition in the pool is itself unambiguous (an
|
|
6127
|
+
* action introduced EXACTLY ONE new value, so there is no question which
|
|
6128
|
+
* value within it is "the" new one) AND the pool's size exactly matches the
|
|
6129
|
+
* number of facets awaiting a value. A transition that introduces more than
|
|
6130
|
+
* one new value is dropped from the pool entirely rather than guessing which
|
|
6131
|
+
* of its values is real; if that drops the pool below (or an unrelated
|
|
6132
|
+
* single-value transition pushes it above) the facet count, the pairing is
|
|
6133
|
+
* no longer guaranteed to line facets up with the RIGHT transition, so
|
|
6134
|
+
* nothing is bound — leaving a facet declared-but-unbound is preferable to
|
|
6135
|
+
* silently wiring it to another facet's (or an unrelated transition's) value.
|
|
6136
|
+
*/
|
|
6137
|
+
function correlateUnreachableNavigateToFacets(facets, actions) {
|
|
6138
|
+
const unambiguousValues = collectNewlyAppearingRequestValueTransitions(actions)
|
|
6139
|
+
.filter((transition) => transition.length === 1)
|
|
6140
|
+
.map((transition) => transition[0]);
|
|
6141
|
+
if (unambiguousValues.length !== facets.length)
|
|
6142
|
+
return null;
|
|
6143
|
+
const byField = new Map();
|
|
6144
|
+
facets.forEach(({ field }, index) => {
|
|
6145
|
+
byField.set(field, unambiguousValues[index]);
|
|
6146
|
+
});
|
|
6147
|
+
return byField;
|
|
6148
|
+
}
|
|
5905
6149
|
/** Exported for unit testing — lets tests drive the multipart-upload code path directly
|
|
5906
6150
|
* without going through the full emitContractTs pipeline. */
|
|
5907
6151
|
function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap, outDiscoveredFields, fieldOptionsMap, outDiscoveredOptionFields, outDiscoveredRawOptionFields, outDiscoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, formSchema = null, personaBindings = new Map(), entryUrlParams = new Map(), shieldedUuids = new Set(), selectResolutions = [], outStructuredKeys = new Map(), rawCodeFields = new Map(), foldReturnSpec = null,
|
|
@@ -6017,6 +6261,46 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6017
6261
|
if (field !== null && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(field))
|
|
6018
6262
|
outDiscoveredFields.add(field);
|
|
6019
6263
|
}
|
|
6264
|
+
// Fallback for a navigateTo step's declared `payloadField` whose extracted
|
|
6265
|
+
// hash literal has ZERO recurrence anywhere in the capture — not merely
|
|
6266
|
+
// blocked by the collision guard above, but genuinely absent from every
|
|
6267
|
+
// action's URL, body, AND headers. `harvestPersonaBindings` can only ever
|
|
6268
|
+
// register the one raw literal it extracted; when the site never replays
|
|
6269
|
+
// that literal verbatim (it's transformed, mapped to a backend code, or
|
|
6270
|
+
// otherwise re-shaped before the real request), the field is structurally
|
|
6271
|
+
// unreachable by the recurrence-anchored path above, exactly like a
|
|
6272
|
+
// declared-but-never-bound schema field.
|
|
6273
|
+
//
|
|
6274
|
+
// Site-agnostic, position/causality-based correlation: a navigateTo step
|
|
6275
|
+
// always precedes the real request its facet affects, so each such
|
|
6276
|
+
// unreachable facet (in flow order) is paired with the next "new" value —
|
|
6277
|
+
// a query-string or body leaf that a later action's capture carries but
|
|
6278
|
+
// its immediately-preceding action's capture does not — found by walking
|
|
6279
|
+
// the actions in order. This is a best-effort structural guarantee, not a
|
|
6280
|
+
// completeness proof: when the adjacent action's own capture carries no
|
|
6281
|
+
// new value at all (the facet's effect is purely server-side / implicit),
|
|
6282
|
+
// or when the correlation is ambiguous (see
|
|
6283
|
+
// `correlateUnreachableNavigateToFacets`), there is nothing safe to splice
|
|
6284
|
+
// and the field(s) stay unbound rather than risk a wrong binding.
|
|
6285
|
+
const appearsAnywhereInCapture = (value) => actions.some((action) => action.capture.url.includes(value) ||
|
|
6286
|
+
(action.capture.requestPostData?.includes(value) ?? false) ||
|
|
6287
|
+
Object.values(action.capture.requestHeaders).some((h) => h.includes(value)));
|
|
6288
|
+
const unreachableNavigateToFacets = extractNavigateToFacetOrder(flowSteps).filter(({ value }) => !appearsAnywhereInCapture(value));
|
|
6289
|
+
if (unreachableNavigateToFacets.length > 0) {
|
|
6290
|
+
const correlated = correlateUnreachableNavigateToFacets(unreachableNavigateToFacets, actions);
|
|
6291
|
+
for (const { field } of correlated === null ? [] : unreachableNavigateToFacets) {
|
|
6292
|
+
const diffed = correlated?.get(field);
|
|
6293
|
+
if (diffed === undefined)
|
|
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);
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6020
6304
|
// Job coordinates from the recon entry URL's query string (e.g.
|
|
6021
6305
|
// `?jobSeqNo=...`). Registered the same way as BaseUrl so every verbatim
|
|
6022
6306
|
// occurrence — and, via length-descending order, embedded substrings like a
|
|
@@ -6280,10 +6564,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6280
6564
|
// dependent-drill-down join field (e.g. a bulk `{"tokens":[...]}` lookup)
|
|
6281
6565
|
// must stay reachable for state threading, not get frozen as caller data.
|
|
6282
6566
|
const rawBodyWithStructuredSubs = parsedBody !== null
|
|
6283
|
-
? applyStructuredValuePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, outStructuredKeys, new Set(
|
|
6284
|
-
...deriveStateVarByValue(prior, cap).keys(),
|
|
6285
|
-
...(joinFieldValuesByStep.get(i) ?? []),
|
|
6286
|
-
]))
|
|
6567
|
+
? applyStructuredValuePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, outStructuredKeys, deriveStateVarByValue(prior, cap), joinFieldValuesByStep.get(i) ?? new Set())
|
|
6287
6568
|
: rawBodyWithFormSubs;
|
|
6288
6569
|
// Whole-value caller coordinates bind here — after structured subs, BEFORE
|
|
6289
6570
|
// state threading — so a composite coordinate (a jobLocation or jobSeqNo
|
|
@@ -11491,10 +11772,25 @@ function computeFlowPayloadFieldNames(flowSteps, vocabulary, env) {
|
|
|
11491
11772
|
for (const step of flowSteps) {
|
|
11492
11773
|
const isObj = typeof step !== "string";
|
|
11493
11774
|
const instruction = isObj ? step.step : step;
|
|
11494
|
-
// navigateTo
|
|
11495
|
-
//
|
|
11496
|
-
|
|
11775
|
+
// A navigateTo step's own instruction text bypasses field resolution —
|
|
11776
|
+
// it's a captured page.goto, not a prose instruction, so there is no
|
|
11777
|
+
// label/quoted-constant to correlate (see emitBrowserFlowTs's stepLiterals
|
|
11778
|
+
// pass). An explicit `payloadField` annotation is a different thing: it is
|
|
11779
|
+
// the author directly naming the facet the navigation target encodes, and
|
|
11780
|
+
// resolveStepPayloadField's own `explicit` short-circuit would honor it
|
|
11781
|
+
// unconditionally — so it must still be registered here, or every
|
|
11782
|
+
// downstream consumer of this accumulator (REST facet-splice, schema
|
|
11783
|
+
// declaration) silently drops the facet the author explicitly declared.
|
|
11784
|
+
if (isObj && step.navigateTo !== undefined) {
|
|
11785
|
+
if (step.payloadField) {
|
|
11786
|
+
payloadFieldNames.add(step.payloadField);
|
|
11787
|
+
// An explicitly declared payloadField is always required at the
|
|
11788
|
+
// schema level, independent of step.optional's execution-skip
|
|
11789
|
+
// semantics (which the browser-flow step record still honors as-is).
|
|
11790
|
+
registerFieldOptionality(step.payloadField, false);
|
|
11791
|
+
}
|
|
11497
11792
|
continue;
|
|
11793
|
+
}
|
|
11498
11794
|
if (isObj && step.emailStep === true)
|
|
11499
11795
|
continue;
|
|
11500
11796
|
if (instruction.includes(RECON_PASSWORD_TOKEN))
|
|
@@ -11504,9 +11800,13 @@ function computeFlowPayloadFieldNames(flowSteps, vocabulary, env) {
|
|
|
11504
11800
|
? resolveCompositePersonaFields(instruction, knownFieldValues)
|
|
11505
11801
|
: null;
|
|
11506
11802
|
const optional = isObj ? step.optional === true : false;
|
|
11803
|
+
const explicit = isObj ? step.payloadField : undefined;
|
|
11507
11804
|
if (field !== null) {
|
|
11508
11805
|
payloadFieldNames.add(field);
|
|
11509
|
-
|
|
11806
|
+
// An explicit payloadField annotation is always required at the
|
|
11807
|
+
// schema level; only the vocabulary/composite-persona heuristic falls
|
|
11808
|
+
// back to step.optional's execution-skip semantics.
|
|
11809
|
+
registerFieldOptionality(field, explicit ? false : optional);
|
|
11510
11810
|
}
|
|
11511
11811
|
if (composite !== null) {
|
|
11512
11812
|
payloadFieldNames.add(composite.fieldA);
|