@enricai/barnacle 1.12.78 → 1.12.80
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/api/helpers/reply.d.ts.map +1 -1
- package/dist/api/helpers/reply.js +5 -2
- package/dist/api/helpers/reply.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/llm/judge.d.ts.map +1 -1
- package/dist/lib/llm/judge.js +6 -1
- package/dist/lib/llm/judge.js.map +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.d.ts +30 -0
- package/dist/lib/llm/judges/verify-step-advanced.d.ts.map +1 -0
- package/dist/lib/llm/judges/verify-step-advanced.js +62 -0
- package/dist/lib/llm/judges/verify-step-advanced.js.map +1 -0
- package/dist/lib/llm/judges/verify-submit.d.ts +7 -1
- package/dist/lib/llm/judges/verify-submit.d.ts.map +1 -1
- package/dist/lib/llm/judges/verify-submit.js +29 -8
- package/dist/lib/llm/judges/verify-submit.js.map +1 -1
- package/dist/lib/llm/schemas.d.ts +8 -0
- package/dist/lib/llm/schemas.d.ts.map +1 -1
- package/dist/lib/llm/schemas.js +21 -13
- package/dist/lib/llm/schemas.js.map +1 -1
- package/dist/lib/logging.d.ts +7 -0
- package/dist/lib/logging.d.ts.map +1 -1
- package/dist/lib/logging.js +19 -1
- package/dist/lib/logging.js.map +1 -1
- package/dist/lib/pii-redaction.d.ts +104 -0
- package/dist/lib/pii-redaction.d.ts.map +1 -0
- package/dist/lib/pii-redaction.js +397 -0
- package/dist/lib/pii-redaction.js.map +1 -0
- package/dist/lib/random.d.ts +3 -1
- package/dist/lib/random.d.ts.map +1 -1
- package/dist/lib/random.js +7 -2
- package/dist/lib/random.js.map +1 -1
- package/dist/lib/telemetry/beacon-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/beacon-capture.js +2 -0
- package/dist/lib/telemetry/beacon-capture.js.map +1 -1
- package/dist/lib/telemetry/call-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/call-capture.js +4 -1
- package/dist/lib/telemetry/call-capture.js.map +1 -1
- package/dist/lib/telemetry/submission-capture.d.ts +11 -1
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-capture.js +32 -3
- package/dist/lib/telemetry/submission-capture.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +8 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +6 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/discover.d.ts.map +1 -1
- package/dist/plugins/discover.js +2 -0
- package/dist/plugins/discover.js.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +23 -6
- package/dist/plugins/loader.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/active-page.d.ts +18 -0
- package/dist/scraper/active-page.d.ts.map +1 -0
- package/dist/scraper/active-page.js +53 -0
- package/dist/scraper/active-page.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/click-settle.d.ts +19 -0
- package/dist/scraper/click-settle.d.ts.map +1 -0
- package/dist/scraper/click-settle.js +65 -0
- package/dist/scraper/click-settle.js.map +1 -0
- package/dist/scraper/deep-locator-candidates.d.ts +20 -1
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
- package/dist/scraper/deep-locator-candidates.js +110 -22
- 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 +355 -6
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +2255 -350
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/navigate.d.ts +3 -3
- package/dist/scraper/navigate.js +4 -4
- package/dist/scraper/navigate.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +34 -2
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +80 -6
- 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/quoted-spans.d.ts +35 -0
- package/dist/scraper/quoted-spans.d.ts.map +1 -0
- package/dist/scraper/quoted-spans.js +83 -0
- package/dist/scraper/quoted-spans.js.map +1 -0
- 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 +4 -2
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +63 -20
- 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-steel.d.ts.map +1 -1
- package/dist/scraper/session-steel.js +16 -2
- package/dist/scraper/session-steel.js.map +1 -1
- package/dist/scraper/session-teardown.d.ts +31 -1
- package/dist/scraper/session-teardown.d.ts.map +1 -1
- package/dist/scraper/session-teardown.js +60 -4
- package/dist/scraper/session-teardown.js.map +1 -1
- package/dist/scraper/spa-readiness.d.ts +13 -1
- package/dist/scraper/spa-readiness.d.ts.map +1 -1
- package/dist/scraper/spa-readiness.js +31 -1
- package/dist/scraper/spa-readiness.js.map +1 -1
- package/dist/scraper/stagehand-guard.d.ts +8 -3
- package/dist/scraper/stagehand-guard.d.ts.map +1 -1
- package/dist/scraper/stagehand-guard.js +181 -13
- package/dist/scraper/stagehand-guard.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/scraper/transient-provider-error.d.ts +19 -0
- package/dist/scraper/transient-provider-error.d.ts.map +1 -0
- package/dist/scraper/transient-provider-error.js +59 -0
- package/dist/scraper/transient-provider-error.js.map +1 -0
- package/dist/scraper/trusted-click.d.ts +122 -0
- package/dist/scraper/trusted-click.d.ts.map +1 -0
- package/dist/scraper/trusted-click.js +201 -0
- package/dist/scraper/trusted-click.js.map +1 -0
- package/dist/scraper/upload-affordance-expr.d.ts +33 -0
- package/dist/scraper/upload-affordance-expr.d.ts.map +1 -0
- package/dist/scraper/upload-affordance-expr.js +91 -0
- package/dist/scraper/upload-affordance-expr.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts +30 -2
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +133 -27
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-capture-rebind.d.ts +11 -0
- package/dist/scripts/recon-capture-rebind.d.ts.map +1 -0
- package/dist/scripts/recon-capture-rebind.js +26 -0
- package/dist/scripts/recon-capture-rebind.js.map +1 -0
- package/dist/scripts/recon-generate.d.ts +159 -8
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +998 -182
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-shared.d.ts +7 -0
- package/dist/scripts/recon-shared.d.ts.map +1 -1
- package/dist/scripts/recon-shared.js +19 -0
- package/dist/scripts/recon-shared.js.map +1 -1
- package/dist/site-plugin.d.ts +8 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/dist/types/pii-redaction.d.ts +39 -0
- package/dist/types/pii-redaction.d.ts.map +1 -0
- package/dist/types/pii-redaction.js +3 -0
- package/dist/types/pii-redaction.js.map +1 -0
- package/package.json +14 -1
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.resolveStepPayloadField = resolveStepPayloadField;
|
|
25
|
+
exports.canonicalPayloadField = canonicalPayloadField;
|
|
25
26
|
exports.buildKnownFieldValues = buildKnownFieldValues;
|
|
26
27
|
exports.extractStepPersonaValue = extractStepPersonaValue;
|
|
27
28
|
exports.extractNavigateToHashFragmentValue = extractNavigateToHashFragmentValue;
|
|
@@ -68,26 +69,34 @@ exports.resolveRecurringNavigateToFacets = resolveRecurringNavigateToFacets;
|
|
|
68
69
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
69
70
|
exports.countSpliceableFacets = countSpliceableFacets;
|
|
70
71
|
exports.spliceFacetsIntoStringVariable = spliceFacetsIntoStringVariable;
|
|
72
|
+
exports.matchFacetRecurrence = matchFacetRecurrence;
|
|
71
73
|
exports.spliceFacetsIntoArrayVariable = spliceFacetsIntoArrayVariable;
|
|
72
74
|
exports.renderGqlVariablesExpr = renderGqlVariablesExpr;
|
|
73
75
|
exports.findAllObjectArrayFields = findAllObjectArrayFields;
|
|
74
76
|
exports.findAllObjectArrayFieldsOrWholeObject = findAllObjectArrayFieldsOrWholeObject;
|
|
77
|
+
exports.collectRequestBodyValuesByKey = collectRequestBodyValuesByKey;
|
|
78
|
+
exports.findFrozenVaryingDrillParams = findFrozenVaryingDrillParams;
|
|
75
79
|
exports.applyDrillParamBindings = applyDrillParamBindings;
|
|
76
80
|
exports.collectResponseLeafValues = collectResponseLeafValues;
|
|
77
81
|
exports.getScanPrimaryCandidateGroupsCallCountForTest = getScanPrimaryCandidateGroupsCallCountForTest;
|
|
78
82
|
exports.resetScanPrimaryCandidateGroupsCallCountForTest = resetScanPrimaryCandidateGroupsCallCountForTest;
|
|
79
83
|
exports.detectDrillDownFoldPlan = detectDrillDownFoldPlan;
|
|
80
84
|
exports.parseFoldReturnSpec = parseFoldReturnSpec;
|
|
85
|
+
exports.parseBrowserFlowOnlyOptIn = parseBrowserFlowOnlyOptIn;
|
|
81
86
|
exports.parseFallbackGateSpec = parseFallbackGateSpec;
|
|
87
|
+
exports.readComparableBodyLeaf = readComparableBodyLeaf;
|
|
82
88
|
exports.collectRequestValuesIncludingHeaders = collectRequestValuesIncludingHeaders;
|
|
83
89
|
exports.getFoldPlanResolutionCallCountForTests = getFoldPlanResolutionCallCountForTests;
|
|
84
90
|
exports.resetFoldPlanResolutionCallCountForTests = resetFoldPlanResolutionCallCountForTests;
|
|
85
91
|
exports.resolveFoldPlan = resolveFoldPlan;
|
|
86
92
|
exports.resolveApplicableFoldPlans = resolveApplicableFoldPlans;
|
|
87
93
|
exports.buildContractChecklist = buildContractChecklist;
|
|
94
|
+
exports.contractUsesApplicantContactSchema = contractUsesApplicantContactSchema;
|
|
88
95
|
exports.emitContractTs = emitContractTs;
|
|
89
96
|
exports.unreferencedRequiredUrlFields = unreferencedRequiredUrlFields;
|
|
90
97
|
exports.assertRequiredUrlFieldsReferenced = assertRequiredUrlFieldsReferenced;
|
|
98
|
+
exports.unreferencedDeclaredPayloadFields = unreferencedDeclaredPayloadFields;
|
|
99
|
+
exports.buildStepInstructionExpr = buildStepInstructionExpr;
|
|
91
100
|
exports.emitConfigManifest = emitConfigManifest;
|
|
92
101
|
exports.emitBrowserFlowTs = emitBrowserFlowTs;
|
|
93
102
|
exports.emitIndexTs = emitIndexTs;
|
|
@@ -106,6 +115,8 @@ const load_value_constraints_1 = require("../recon/load-value-constraints");
|
|
|
106
115
|
const load_vocabulary_1 = require("../recon/load-vocabulary");
|
|
107
116
|
const value_constraints_1 = require("../recon/value-constraints");
|
|
108
117
|
const vocabulary_1 = require("../recon/vocabulary");
|
|
118
|
+
const flow_runner_1 = require("../scraper/flow-runner");
|
|
119
|
+
const quoted_spans_1 = require("../scraper/quoted-spans");
|
|
109
120
|
const recon_shared_1 = require("./recon-shared");
|
|
110
121
|
const logger = (0, logging_1.getScriptLogger)("recon-generate");
|
|
111
122
|
/**
|
|
@@ -138,39 +149,54 @@ const RESERVED_ENV_TOKEN = /\$\{RECON_[A-Z0-9_]*\}/;
|
|
|
138
149
|
* (never through) vocabulary/payload-field resolution.
|
|
139
150
|
*/
|
|
140
151
|
const RECON_PASSWORD_TOKEN = `$${"{RECON_PASSWORD}"}`;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*/
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
value: instruction.slice(m.index + 1, m.index + m[0].length - 1),
|
|
152
|
+
const DOUBLE_QUOTED_SPAN = /"([^"]+)"/g;
|
|
153
|
+
/**
|
|
154
|
+
* Quoted spans of a flow-step instruction. The runtime parsers only understand
|
|
155
|
+
* single quotes, so a double-quoted span is consulted only when the step has no
|
|
156
|
+
* single-quoted one — a mixed instruction keeps its single-quote grammar, and a
|
|
157
|
+
* double-quoted-only step still has a spliceable value instead of being skipped.
|
|
158
|
+
*/
|
|
159
|
+
function findInstructionSpans(instruction) {
|
|
160
|
+
const single = (0, quoted_spans_1.findQuotedSpans)(instruction);
|
|
161
|
+
if (single.length > 0)
|
|
162
|
+
return single;
|
|
163
|
+
return Array.from(instruction.matchAll(DOUBLE_QUOTED_SPAN), (match) => ({
|
|
164
|
+
text: match[1] ?? "",
|
|
165
|
+
start: match.index ?? 0,
|
|
166
|
+
end: (match.index ?? 0) + match[0].length,
|
|
157
167
|
}));
|
|
158
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* A quoted span that names the widget or question a step targets rather than
|
|
171
|
+
* the value it carries: followed by a widget noun ("'State' prompt selector"),
|
|
172
|
+
* or introduced by "for"/"from" ("for the 'State' question").
|
|
173
|
+
*/
|
|
174
|
+
function namesAWidgetLabel(instruction, span) {
|
|
175
|
+
const before = instruction.slice(Math.max(0, span.start - 25), span.start);
|
|
176
|
+
const after = instruction.slice(span.end);
|
|
177
|
+
return (/^\s*(?:prompt\s+selector|selector|dropdown|drop-down|multiselect|typeahead|field|question|list|menu|widget)\b/i.test(after) || /\b(?:for|from)(?:\s+the)?(?:\s+(?:question|answer))?\s*$/i.test(before));
|
|
178
|
+
}
|
|
159
179
|
/**
|
|
160
180
|
* Picks which quoted span in an instruction is the persona VALUE, per the
|
|
161
181
|
* grammar recon-flow steps use: a `Select`/`Choose`/`Pick` step names the
|
|
162
|
-
* ANSWER
|
|
163
|
-
*
|
|
164
|
-
*
|
|
182
|
+
* ANSWER after the verb (wherever the verb sits, so a leading context phrase
|
|
183
|
+
* does not hide it), so the value is the first span following the verb that is
|
|
184
|
+
* not a widget label — "choose the 'State' field, then choose 'Texas'" must
|
|
185
|
+
* yield Texas, not the label. Any other step (`Fill`/`Enter`/`Type`) names the
|
|
186
|
+
* field label first and the value last, so it is the LAST quoted span.
|
|
165
187
|
*/
|
|
166
188
|
function pickValueSpan(instruction, spans) {
|
|
167
|
-
|
|
189
|
+
const verb = /\b(select|choose|pick)\b/i.exec(instruction);
|
|
190
|
+
const afterVerb = verb ? spans.filter((span) => span.start >= verb.index + verb[0].length) : [];
|
|
191
|
+
const answer = afterVerb.find((span) => !namesAWidgetLabel(instruction, span)) ?? afterVerb[0];
|
|
192
|
+
return answer ?? spans[spans.length - 1];
|
|
168
193
|
}
|
|
169
194
|
/**
|
|
170
195
|
* Locates the splice site in a flow-step instruction as `{ before, matched,
|
|
171
196
|
* after }` slices of the ORIGINAL instruction — a reserved `${RECON_*}` env
|
|
172
197
|
* token when present (preferred, since it names its own site unambiguously),
|
|
173
|
-
* otherwise the quoted VALUE span per {@link pickValueSpan}'s verb-class rule
|
|
198
|
+
* otherwise the quoted VALUE span per {@link pickValueSpan}'s verb-class rule
|
|
199
|
+
* (its quote delimiters stay in `before`/`after`, so a splice remains quoted).
|
|
174
200
|
* This is the position-aware counterpart to {@link extractStepPersonaValue}:
|
|
175
201
|
* that function only needs the extracted string, this one needs the actual
|
|
176
202
|
* before/after slices so a template literal can be rebuilt around the site.
|
|
@@ -186,14 +212,14 @@ function locateSpliceSite(instruction) {
|
|
|
186
212
|
after: instruction.slice(envToken.index + envToken[0].length),
|
|
187
213
|
};
|
|
188
214
|
}
|
|
189
|
-
const spans =
|
|
215
|
+
const spans = findInstructionSpans(instruction);
|
|
190
216
|
if (spans.length === 0)
|
|
191
217
|
return null;
|
|
192
218
|
const span = pickValueSpan(instruction, spans);
|
|
193
219
|
return {
|
|
194
|
-
before: instruction.slice(0, span.
|
|
195
|
-
matched: instruction.slice(span.
|
|
196
|
-
after: instruction.slice(span.
|
|
220
|
+
before: instruction.slice(0, span.start + 1),
|
|
221
|
+
matched: instruction.slice(span.start + 1, span.end - 1),
|
|
222
|
+
after: instruction.slice(span.end - 1),
|
|
197
223
|
};
|
|
198
224
|
}
|
|
199
225
|
/**
|
|
@@ -204,15 +230,15 @@ function locateSpliceSite(instruction) {
|
|
|
204
230
|
* can never be mistaken for the data it merely mentions.
|
|
205
231
|
*/
|
|
206
232
|
function namesAControl(instruction) {
|
|
207
|
-
return
|
|
233
|
+
return findInstructionSpans(instruction).some((span) => /^\s*(button|link|tab)\b/i.test(instruction.slice(span.end)));
|
|
208
234
|
}
|
|
209
235
|
/** The quoted VALUE a Select/Choose/Fill step carries, per {@link pickValueSpan}'s
|
|
210
236
|
* grammar rule — used to validate a vocabulary match's ANSWER, not just its label. */
|
|
211
237
|
function pickedQuotedValue(instruction) {
|
|
212
|
-
const spans =
|
|
238
|
+
const spans = findInstructionSpans(instruction);
|
|
213
239
|
if (spans.length === 0)
|
|
214
240
|
return null;
|
|
215
|
-
const value = pickValueSpan(instruction, spans).
|
|
241
|
+
const value = pickValueSpan(instruction, spans).text;
|
|
216
242
|
return value.length > 0 ? value : null;
|
|
217
243
|
}
|
|
218
244
|
/**
|
|
@@ -251,9 +277,12 @@ function toPascalCase(siteId) {
|
|
|
251
277
|
* label match names, or it is not that field's datum (e.g. a device-type
|
|
252
278
|
* dropdown's `'Mobile'` looks like a phone-number field by label alone, but
|
|
253
279
|
* is nothing like the number the Fill step already bound).
|
|
280
|
+
* @param canonicalise map a vocabulary row's name onto the equivalent
|
|
281
|
+
* `ApplicantContactSchema` key ({@link canonicalPayloadField}); set by the
|
|
282
|
+
* job-application emit paths whose contract extends that base schema
|
|
254
283
|
* @returns the PascalCase payload field name to splice, or null to keep literal
|
|
255
284
|
*/
|
|
256
|
-
function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = vocabulary_1.EMPTY_VOCABULARY, knownFieldValues = EMPTY_KNOWN_FIELD_VALUES) {
|
|
285
|
+
function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = vocabulary_1.EMPTY_VOCABULARY, knownFieldValues = EMPTY_KNOWN_FIELD_VALUES, canonicalise = false) {
|
|
257
286
|
if (forceNone)
|
|
258
287
|
return null;
|
|
259
288
|
if (explicit)
|
|
@@ -264,7 +293,7 @@ function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary =
|
|
|
264
293
|
return null;
|
|
265
294
|
// A quoted literal or a reserved ${RECON_*} env token IS the recon constant
|
|
266
295
|
// this step would replace, so it is spliceable on its own.
|
|
267
|
-
const hasQuotedConstant =
|
|
296
|
+
const hasQuotedConstant = findInstructionSpans(instruction).length > 0 || RESERVED_ENV_TOKEN.test(instruction);
|
|
268
297
|
// A dropdown step carries no constant to replace, so a label match alone can't
|
|
269
298
|
// tell "select the test candidate's state" (the caller's data) from "select the
|
|
270
299
|
// neighborhood from the Country dropdown" (a facet that merely says Country).
|
|
@@ -283,18 +312,49 @@ function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary =
|
|
|
283
312
|
for (const [rx, field] of vocabulary.table) {
|
|
284
313
|
if (!rx.test(instruction))
|
|
285
314
|
continue;
|
|
315
|
+
const resolved = canonicalise ? canonicalPayloadField(field) : field;
|
|
286
316
|
if (!isSelectStep)
|
|
287
|
-
return
|
|
317
|
+
return resolved;
|
|
288
318
|
const answer = pickedQuotedValue(instruction);
|
|
289
319
|
if (answer !== null && OPERATIONAL_ANSWER.test(answer.trim()))
|
|
290
320
|
return null;
|
|
291
|
-
const known = knownFieldValues.get(
|
|
321
|
+
const known = knownFieldValues.get(resolved);
|
|
292
322
|
if (known !== undefined && answer !== known)
|
|
293
323
|
return null;
|
|
294
|
-
return
|
|
324
|
+
return resolved;
|
|
295
325
|
}
|
|
296
326
|
return null;
|
|
297
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Maps a vocabulary row's field name onto the base schema's equivalent so a
|
|
330
|
+
* consumer's `MobilePhone` does not become an extra required key beside
|
|
331
|
+
* `Phone`. Names the base schema has no equivalent for pass through.
|
|
332
|
+
*/
|
|
333
|
+
function canonicalPayloadField(field) {
|
|
334
|
+
if (applicantContactFieldNames.has(field) || baseContractFieldNames.has(field))
|
|
335
|
+
return field;
|
|
336
|
+
const canonical = (0, ats_field_vocabulary_1.resolveCanonicalAtsFieldName)(field);
|
|
337
|
+
return canonical !== null && applicantContactFieldNames.has(canonical) ? canonical : field;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The single decision for which payload field a fill step carries: the
|
|
341
|
+
* vocabulary/explicit field, else the instruction's own label — never when the
|
|
342
|
+
* author opted out (`payloadFieldNone`) or the vocabulary excludes the step.
|
|
343
|
+
* Shared so known-field and persona-binding registration cannot drift apart.
|
|
344
|
+
*/
|
|
345
|
+
function fieldForFillStep(step, vocabulary, knownFieldValues, canonicalise = false) {
|
|
346
|
+
const isObj = typeof step !== "string";
|
|
347
|
+
const instruction = isObj ? step.step : step;
|
|
348
|
+
const vocabField = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues, canonicalise);
|
|
349
|
+
if (vocabField !== null)
|
|
350
|
+
return vocabField;
|
|
351
|
+
if (isObj && step.payloadFieldNone)
|
|
352
|
+
return null;
|
|
353
|
+
if (vocabulary.exclusions.some((rx) => rx.test(instruction)))
|
|
354
|
+
return null;
|
|
355
|
+
const labelField = deriveFillLabelField(instruction);
|
|
356
|
+
return canonicalise && labelField !== null ? canonicalPayloadField(labelField) : labelField;
|
|
357
|
+
}
|
|
298
358
|
/**
|
|
299
359
|
* Field→value pairs the flow establishes unambiguously via a Fill/Enter/Type
|
|
300
360
|
* step — the applicant's own datum, per {@link deriveFillLabelField}'s same
|
|
@@ -304,14 +364,14 @@ function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary =
|
|
|
304
364
|
* (a field whose label mentions "phone" but whose select answer is a device
|
|
305
365
|
* type, not a number).
|
|
306
366
|
*/
|
|
307
|
-
function buildKnownFieldValues(flowSteps, vocabulary, env) {
|
|
367
|
+
function buildKnownFieldValues(flowSteps, vocabulary, env, canonicalise = false) {
|
|
308
368
|
const known = new Map();
|
|
309
369
|
for (const step of flowSteps) {
|
|
310
370
|
const isObj = typeof step !== "string";
|
|
311
371
|
const instruction = isObj ? step.step : step;
|
|
312
372
|
if (!/^\s*(?:fill(?:\s+in)?|enter|type)\b/i.test(instruction))
|
|
313
373
|
continue;
|
|
314
|
-
const field =
|
|
374
|
+
const field = fieldForFillStep(step, vocabulary, undefined, canonicalise);
|
|
315
375
|
if (field === null)
|
|
316
376
|
continue;
|
|
317
377
|
const value = extractStepPersonaValue(instruction, env);
|
|
@@ -331,9 +391,9 @@ function buildKnownFieldValues(flowSteps, vocabulary, env) {
|
|
|
331
391
|
* or the value-identity substitution silently misses.
|
|
332
392
|
*
|
|
333
393
|
* Two grammar facts, both verified against real ATS flows, drive the rule:
|
|
334
|
-
* 1.
|
|
335
|
-
*
|
|
336
|
-
*
|
|
394
|
+
* 1. A letter-flanked apostrophe ("the candidate's first name 'Reginald'",
|
|
395
|
+
* "Don't") is not a quote delimiter; the shared tokenizer skips it, so the
|
|
396
|
+
* span is `Reginald`, not `s first name `.
|
|
337
397
|
* 2. A `Select`/`Choose` step names the ANSWER first, then the question
|
|
338
398
|
* ("Select 'No' for the 'sponsorship' question"), so the value is the
|
|
339
399
|
* FIRST quoted token; a `Fill`/`Enter`/`Type` step names the field label
|
|
@@ -360,10 +420,10 @@ function extractStepPersonaValue(instruction, env) {
|
|
|
360
420
|
if (resolved)
|
|
361
421
|
return resolved;
|
|
362
422
|
}
|
|
363
|
-
const spans =
|
|
423
|
+
const spans = findInstructionSpans(instruction);
|
|
364
424
|
if (spans.length === 0)
|
|
365
425
|
return null;
|
|
366
|
-
const value = pickValueSpan(instruction, spans).
|
|
426
|
+
const value = pickValueSpan(instruction, spans).text;
|
|
367
427
|
return value.length > 0 ? value : null;
|
|
368
428
|
}
|
|
369
429
|
/**
|
|
@@ -406,12 +466,26 @@ function extractNavigateToHashFragmentValue(url, previousHash) {
|
|
|
406
466
|
* hash-bearing navigateTo; a hashless reset navigation must not erase it, or
|
|
407
467
|
* the next cumulative hash would be taken whole and never recur in a request.
|
|
408
468
|
*/
|
|
409
|
-
function walkNavigateToFacets(flowSteps) {
|
|
469
|
+
function walkNavigateToFacets(flowSteps, env) {
|
|
410
470
|
const facets = [];
|
|
411
471
|
let baselineHash;
|
|
412
472
|
for (const step of flowSteps) {
|
|
413
|
-
if (typeof step === "string"
|
|
473
|
+
if (typeof step === "string")
|
|
474
|
+
continue;
|
|
475
|
+
if (step.navigateTo === undefined) {
|
|
476
|
+
if (step.payloadField && !step.payloadFieldNone) {
|
|
477
|
+
facets.push({
|
|
478
|
+
value: extractStepPersonaValue(step.step, env),
|
|
479
|
+
field: step.payloadField,
|
|
480
|
+
optional: step.optional === true,
|
|
481
|
+
origin: "step",
|
|
482
|
+
literals: collectStepFacetLiterals(step.step, env),
|
|
483
|
+
kind: deriveFacetKind(step.payloadField),
|
|
484
|
+
kindHints: [],
|
|
485
|
+
});
|
|
486
|
+
}
|
|
414
487
|
continue;
|
|
488
|
+
}
|
|
415
489
|
const hashIndex = step.navigateTo.indexOf("#");
|
|
416
490
|
const currentHash = hashIndex === -1 ? undefined : step.navigateTo.slice(hashIndex + 1);
|
|
417
491
|
if (step.payloadField) {
|
|
@@ -419,6 +493,10 @@ function walkNavigateToFacets(flowSteps) {
|
|
|
419
493
|
value: extractNavigateToHashFragmentValue(step.navigateTo, baselineHash),
|
|
420
494
|
field: step.payloadField,
|
|
421
495
|
optional: step.optional === true,
|
|
496
|
+
origin: "navigateTo",
|
|
497
|
+
literals: collectHashFacetLiterals(step.navigateTo, baselineHash),
|
|
498
|
+
kind: deriveFacetKind(step.payloadField),
|
|
499
|
+
kindHints: collectHashFacetKindHints(step.navigateTo, baselineHash),
|
|
422
500
|
});
|
|
423
501
|
}
|
|
424
502
|
if (currentHash !== undefined)
|
|
@@ -426,6 +504,60 @@ function walkNavigateToFacets(flowSteps) {
|
|
|
426
504
|
}
|
|
427
505
|
return facets;
|
|
428
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Derives a facet's element kind from its declared field name (`tripLength`
|
|
509
|
+
* and `TripLength` both yield `trip length`), so a matcher can tell which
|
|
510
|
+
* captured element a literal belongs to without any site vocabulary.
|
|
511
|
+
*/
|
|
512
|
+
function deriveFacetKind(field) {
|
|
513
|
+
return field
|
|
514
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
515
|
+
.replace(/[_\-\s]+/g, " ")
|
|
516
|
+
.trim()
|
|
517
|
+
.toLowerCase();
|
|
518
|
+
}
|
|
519
|
+
function uniqueLiterals(primary, others) {
|
|
520
|
+
return [...new Set([...(primary === null ? [] : [primary]), ...others])].filter((literal) => literal.length > 0);
|
|
521
|
+
}
|
|
522
|
+
function collectStepFacetLiterals(instruction, env) {
|
|
523
|
+
const primary = extractStepPersonaValue(instruction, env);
|
|
524
|
+
return uniqueLiterals(primary, findInstructionSpans(instruction).map((span) => span.text));
|
|
525
|
+
}
|
|
526
|
+
function collectHashFacetLiterals(url, previousHash) {
|
|
527
|
+
const primary = extractNavigateToHashFragmentValue(url, previousHash);
|
|
528
|
+
const hashIndex = url.indexOf("#");
|
|
529
|
+
const rawTokens = hashIndex === -1 || previousHash === undefined
|
|
530
|
+
? []
|
|
531
|
+
: splitHashTokens(url.slice(hashIndex + 1)).filter((token) => !splitHashTokens(previousHash).includes(token));
|
|
532
|
+
return uniqueLiterals(primary, rawTokens);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Further element kinds a facet's own hash names (`ship` in `#/ship/<slug>` or
|
|
536
|
+
* `ship=<slug>`, `ports` in `ports_<slug>`), for a field name that does not
|
|
537
|
+
* itself name the element kind. Kept beside the binding rather than on it so
|
|
538
|
+
* the binding's declared shape stays the contract callers build and compare.
|
|
539
|
+
*/
|
|
540
|
+
const FACET_KIND_HINTS = new WeakMap();
|
|
541
|
+
/**
|
|
542
|
+
* Element kinds a hash facet's own tokens name: the key of a `key=value` pair,
|
|
543
|
+
* the path segment ahead of the value, and the `<kind>_` prefix of a slug.
|
|
544
|
+
*/
|
|
545
|
+
function collectHashFacetKindHints(url, previousHash) {
|
|
546
|
+
const hashIndex = url.indexOf("#");
|
|
547
|
+
if (hashIndex === -1)
|
|
548
|
+
return [];
|
|
549
|
+
const previousTokens = new Set(previousHash === undefined ? [] : splitHashTokens(previousHash));
|
|
550
|
+
const tokens = splitHashTokens(url.slice(hashIndex + 1));
|
|
551
|
+
const fresh = tokens.filter((token) => !previousTokens.has(token));
|
|
552
|
+
const valueToken = fresh[fresh.length - 1] ?? tokens[tokens.length - 1];
|
|
553
|
+
if (valueToken === undefined)
|
|
554
|
+
return [];
|
|
555
|
+
const value = stripHashKeyPrefix(valueToken);
|
|
556
|
+
const keyOfPair = valueToken.includes("=") ? valueToken.slice(0, valueToken.indexOf("=")) : "";
|
|
557
|
+
const ahead = tokens[tokens.indexOf(valueToken) - 1] ?? "";
|
|
558
|
+
const slugPrefix = value.includes("_") ? value.slice(0, value.indexOf("_")) : "";
|
|
559
|
+
return [keyOfPair, ahead, slugPrefix].filter((hint) => /^[A-Za-z][A-Za-z0-9 _-]*$/.test(hint));
|
|
560
|
+
}
|
|
429
561
|
function splitHashTokens(hash) {
|
|
430
562
|
return hash.split(/[,/;&]/).filter((token) => token.length > 0);
|
|
431
563
|
}
|
|
@@ -456,7 +588,7 @@ function stripHashKeyPrefix(token) {
|
|
|
456
588
|
function deriveFillLabelField(instruction) {
|
|
457
589
|
if (!/^\s*(?:fill(?:\s+in)?|enter|type)\b/i.test(instruction))
|
|
458
590
|
return null;
|
|
459
|
-
if (
|
|
591
|
+
if (findInstructionSpans(instruction).length === 0)
|
|
460
592
|
return null;
|
|
461
593
|
const label = /\b(?:fill(?:\s+in)?|enter|type)\s+(?:in\s+)?the\s+(.+?)\s+field\b/i.exec(instruction)?.[1];
|
|
462
594
|
if (label === undefined)
|
|
@@ -479,7 +611,7 @@ function deriveFillLabelField(instruction) {
|
|
|
479
611
|
function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
480
612
|
const bindings = new Map();
|
|
481
613
|
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary, env);
|
|
482
|
-
for (const facet of walkNavigateToFacets(flowSteps)) {
|
|
614
|
+
for (const facet of walkNavigateToFacets(flowSteps, env)) {
|
|
483
615
|
if (facet.value !== null && !bindings.has(facet.value)) {
|
|
484
616
|
bindings.set(facet.value, `payload.${facet.field}`);
|
|
485
617
|
}
|
|
@@ -496,16 +628,7 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
496
628
|
// fragment, not anything extractStepPersonaValue could find in `step.step`.
|
|
497
629
|
if (isObj && step.navigateTo !== undefined)
|
|
498
630
|
continue;
|
|
499
|
-
const
|
|
500
|
-
// Vocabulary wins outright. Only on a miss do we fall back to deriving the
|
|
501
|
-
// field from the instruction's own label — and never when the author opted
|
|
502
|
-
// the step out (`payloadFieldNone`) or the vocabulary explicitly excluded it.
|
|
503
|
-
const field = vocabField ??
|
|
504
|
-
(isObj && step.payloadFieldNone
|
|
505
|
-
? null
|
|
506
|
-
: vocabulary.exclusions.some((rx) => rx.test(instruction))
|
|
507
|
-
? null
|
|
508
|
-
: deriveFillLabelField(instruction));
|
|
631
|
+
const field = fieldForFillStep(step, vocabulary, knownFieldValues);
|
|
509
632
|
if (field === null)
|
|
510
633
|
continue;
|
|
511
634
|
const value = extractStepPersonaValue(instruction, env);
|
|
@@ -531,12 +654,22 @@ function harvestPersonaBindings(flowSteps, vocabulary, env) {
|
|
|
531
654
|
* aligned with the declared facets; {@link resolveRecurringNavigateToFacets}
|
|
532
655
|
* drops the empty literal itself and keeps only a correlated value.
|
|
533
656
|
*/
|
|
534
|
-
function extractNavigateToFacetOrder(flowSteps) {
|
|
535
|
-
return walkNavigateToFacets(flowSteps)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
657
|
+
function extractNavigateToFacetOrder(flowSteps, env = process.env) {
|
|
658
|
+
return walkNavigateToFacets(flowSteps, env)
|
|
659
|
+
.filter((facet) => facet.origin === "navigateTo" || facet.value !== null)
|
|
660
|
+
.map((facet) => {
|
|
661
|
+
const binding = {
|
|
662
|
+
value: facet.value ?? "",
|
|
663
|
+
field: facet.field,
|
|
664
|
+
optional: facet.optional,
|
|
665
|
+
literals: facet.literals,
|
|
666
|
+
kind: facet.kind,
|
|
667
|
+
...(facet.origin === "step" ? { origin: facet.origin } : {}),
|
|
668
|
+
};
|
|
669
|
+
if (facet.kindHints.length > 0)
|
|
670
|
+
FACET_KIND_HINTS.set(binding, facet.kindHints);
|
|
671
|
+
return binding;
|
|
672
|
+
});
|
|
540
673
|
}
|
|
541
674
|
/**
|
|
542
675
|
* Detects a flow step whose quoted VALUE is the space-joined concatenation of
|
|
@@ -550,10 +683,10 @@ function extractNavigateToFacetOrder(flowSteps) {
|
|
|
550
683
|
* null when the step's value is not such a concatenation
|
|
551
684
|
*/
|
|
552
685
|
function resolveCompositePersonaFields(instruction, knownFieldValues) {
|
|
553
|
-
const spans =
|
|
686
|
+
const spans = findInstructionSpans(instruction);
|
|
554
687
|
if (spans.length === 0)
|
|
555
688
|
return null;
|
|
556
|
-
const value = pickValueSpan(instruction, spans).
|
|
689
|
+
const value = pickValueSpan(instruction, spans).text;
|
|
557
690
|
if (value.length === 0)
|
|
558
691
|
return null;
|
|
559
692
|
for (const [fieldA, valueA] of knownFieldValues) {
|
|
@@ -1296,6 +1429,10 @@ function deriveBaseUrl(captures, ownBackendHostnames, submitPatterns = null) {
|
|
|
1296
1429
|
return dominantHostOrigin(sameDomainCandidates);
|
|
1297
1430
|
}
|
|
1298
1431
|
const candidates = captures.filter((c) => (0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, null));
|
|
1432
|
+
if (candidates.length === 0) {
|
|
1433
|
+
const captured = [...new Set(captures.map((c) => captureHostname(c.url)))].join(", ");
|
|
1434
|
+
throw new Error(`ownBackendHostnames matched no capture: declared [${ownBackendHostnames.join(", ")}], captured hosts [${captured}]`);
|
|
1435
|
+
}
|
|
1299
1436
|
return dominantHostOrigin(candidates);
|
|
1300
1437
|
}
|
|
1301
1438
|
function firstCaptureOrigin(captures) {
|
|
@@ -1308,7 +1445,7 @@ function firstCaptureOrigin(captures) {
|
|
|
1308
1445
|
// try next
|
|
1309
1446
|
}
|
|
1310
1447
|
}
|
|
1311
|
-
|
|
1448
|
+
throw new Error(`no capture has a parseable URL to derive a base URL from (${captures.length} candidate captures)`);
|
|
1312
1449
|
}
|
|
1313
1450
|
/**
|
|
1314
1451
|
* Extracts caller-supplied job/context coordinates from the recon ENTRY URL's
|
|
@@ -3801,11 +3938,8 @@ function buildSelectOptionResolutions(flowSteps, captures, vocabulary, env) {
|
|
|
3801
3938
|
// returns first-quote when the sentence STARTS with select/choose/pick; a
|
|
3802
3939
|
// dropdown step phrased "On the X step, select 'No' in the '…?' dropdown"
|
|
3803
3940
|
// starts with "On", so read the first quote directly here for id= steps.
|
|
3804
|
-
const
|
|
3805
|
-
|
|
3806
|
-
const m = /'([^']*)'/.exec(cleaned);
|
|
3807
|
-
return m && m[1].length > 0 ? m[1] : null;
|
|
3808
|
-
})();
|
|
3941
|
+
const firstQuoteText = findInstructionSpans(instruction)[0]?.text;
|
|
3942
|
+
const firstQuoteLabel = firstQuoteText ? firstQuoteText : null;
|
|
3809
3943
|
const label = idMatch ? firstQuoteLabel : extractStepPersonaValue(instruction, env);
|
|
3810
3944
|
if (label === null)
|
|
3811
3945
|
continue;
|
|
@@ -5995,9 +6129,14 @@ function applyScalarFacetSplicePayloadSubstitutions(template, inputBody, navigat
|
|
|
5995
6129
|
continue;
|
|
5996
6130
|
const literal = JSON.stringify(value);
|
|
5997
6131
|
const fallback = facet.optional ? ` ?? ${literal}` : "";
|
|
5998
|
-
const
|
|
5999
|
-
|
|
6000
|
-
|
|
6132
|
+
const casing = facetCaseMethod(String(value), facet);
|
|
6133
|
+
const expression = facetPayloadExpression(facet, fallback);
|
|
6134
|
+
const cased = casing ? `String(${expression}).${casing}()` : undefined;
|
|
6135
|
+
const replacement = cased
|
|
6136
|
+
? `\${JSON.stringify(${cased})}`
|
|
6137
|
+
: typeof value === "number" || facet.optional
|
|
6138
|
+
? `\${JSON.stringify(${expression})}`
|
|
6139
|
+
: `"\${${expression}}"`;
|
|
6001
6140
|
for (const sep of [":", ": "]) {
|
|
6002
6141
|
if (typeof value === "number") {
|
|
6003
6142
|
const re = new RegExp(`${sep}${literal}(?![\\w.])`, "g");
|
|
@@ -6044,7 +6183,7 @@ function* walkArrayLeaves(value, path = []) {
|
|
|
6044
6183
|
* an `undefined` and no call site renders the array through a different
|
|
6045
6184
|
* construction. Non-facet elements stay literal.
|
|
6046
6185
|
*/
|
|
6047
|
-
function applyArrayFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets, ownedArrayPathKeys) {
|
|
6186
|
+
function applyArrayFacetSplicePayloadSubstitutions(template, inputBody, navigateToFacets, ownedArrayPathKeys, siblingBodies = []) {
|
|
6048
6187
|
if (navigateToFacets.length === 0)
|
|
6049
6188
|
return template;
|
|
6050
6189
|
if (inputBody === null || typeof inputBody !== "object")
|
|
@@ -6057,11 +6196,73 @@ function applyArrayFacetSplicePayloadSubstitutions(template, inputBody, navigate
|
|
|
6057
6196
|
!containsFacetArray(value, navigateToFacets)) {
|
|
6058
6197
|
continue;
|
|
6059
6198
|
}
|
|
6060
|
-
const
|
|
6199
|
+
const absentFacets = collectSiblingArrayFacets(value, path, siblingBodies, navigateToFacets);
|
|
6200
|
+
const absentFields = new Set(absentFacets.map(({ facet }) => facet.field));
|
|
6201
|
+
const expression = renderFacetValueExpression(insertAbsentFacetElements(value, absentFacets), navigateToFacets.map((facet) => absentFields.has(facet.field) ? { ...facet, optional: true } : facet));
|
|
6061
6202
|
result = replaceJsonArrayOccurrences(result, value, `\${JSON.stringify(${expression})}`);
|
|
6062
6203
|
}
|
|
6063
6204
|
return result;
|
|
6064
6205
|
}
|
|
6206
|
+
/**
|
|
6207
|
+
* Inserts each sibling-only element next to its nearest neighbour that `own`
|
|
6208
|
+
* (or an earlier insertion) also carries, so the emitted order follows the
|
|
6209
|
+
* capture it came from. With no shared neighbour it is appended.
|
|
6210
|
+
*/
|
|
6211
|
+
function insertAbsentFacetElements(own, absent) {
|
|
6212
|
+
const merged = [...own];
|
|
6213
|
+
const indexOfElement = (target) => merged.findIndex((candidate) => JSON.stringify(candidate) === JSON.stringify(target));
|
|
6214
|
+
for (const { element, before, after } of absent) {
|
|
6215
|
+
const previous = [...before]
|
|
6216
|
+
.reverse()
|
|
6217
|
+
.map(indexOfElement)
|
|
6218
|
+
.find((index) => index >= 0);
|
|
6219
|
+
if (previous !== undefined) {
|
|
6220
|
+
merged.splice(previous + 1, 0, element);
|
|
6221
|
+
continue;
|
|
6222
|
+
}
|
|
6223
|
+
const next = after.map(indexOfElement).find((index) => index >= 0);
|
|
6224
|
+
if (next === undefined)
|
|
6225
|
+
merged.push(element);
|
|
6226
|
+
else
|
|
6227
|
+
merged.splice(next, 0, element);
|
|
6228
|
+
}
|
|
6229
|
+
return merged;
|
|
6230
|
+
}
|
|
6231
|
+
/**
|
|
6232
|
+
* Facet-bearing elements that same-endpoint sibling captures carry at the
|
|
6233
|
+
* array `path` but `own` does not. A fold drill re-issues ONE representative
|
|
6234
|
+
* capture per item, so a facet only a dropped sibling's differently shaped
|
|
6235
|
+
* array carried would otherwise never reach the emitted body. Each facet is
|
|
6236
|
+
* taken once, at its first occurrence, and only for facets `own` leaves out.
|
|
6237
|
+
*/
|
|
6238
|
+
function collectSiblingArrayFacets(own, path, siblingBodies, facets) {
|
|
6239
|
+
const present = new Set(own.flatMap((element) => typeof element === "string" ? [matchArrayElementFacet(element, facets)?.field] : []));
|
|
6240
|
+
const key = arrayPathKey(path);
|
|
6241
|
+
const absent = [];
|
|
6242
|
+
for (const body of siblingBodies) {
|
|
6243
|
+
if (body === null || typeof body !== "object")
|
|
6244
|
+
continue;
|
|
6245
|
+
for (const { value, path: siblingPath } of walkArrayLeaves(body)) {
|
|
6246
|
+
if (arrayPathKey(siblingPath) !== key)
|
|
6247
|
+
continue;
|
|
6248
|
+
for (const [index, element] of value.entries()) {
|
|
6249
|
+
if (typeof element !== "string")
|
|
6250
|
+
continue;
|
|
6251
|
+
const facet = matchArrayElementFacet(element, facets);
|
|
6252
|
+
if (facet === undefined || present.has(facet.field))
|
|
6253
|
+
continue;
|
|
6254
|
+
present.add(facet.field);
|
|
6255
|
+
absent.push({
|
|
6256
|
+
element,
|
|
6257
|
+
facet,
|
|
6258
|
+
before: value.slice(0, index),
|
|
6259
|
+
after: value.slice(index + 1),
|
|
6260
|
+
});
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
return absent;
|
|
6265
|
+
}
|
|
6065
6266
|
/** Whether `value` is, or contains at any depth, an array with a
|
|
6066
6267
|
* facet-bearing string element. */
|
|
6067
6268
|
function containsFacetArray(value, facets) {
|
|
@@ -6460,6 +6661,16 @@ function parseUrlQueryParams(url) {
|
|
|
6460
6661
|
}
|
|
6461
6662
|
return params;
|
|
6462
6663
|
}
|
|
6664
|
+
/** Every query value of a URL, repeated keys included (`?f=a&f=b`), where
|
|
6665
|
+
* {@link parseUrlQueryParams} keeps only the last per key. */
|
|
6666
|
+
function parseUrlQueryValues(url) {
|
|
6667
|
+
try {
|
|
6668
|
+
return [...new URL(url).searchParams.values()];
|
|
6669
|
+
}
|
|
6670
|
+
catch {
|
|
6671
|
+
return [];
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6463
6674
|
/** Parses a captured JSON request body into its string leaf values. Returns
|
|
6464
6675
|
* an empty array when the body is absent or not valid JSON. */
|
|
6465
6676
|
function parseBodyLeafValues(requestPostData) {
|
|
@@ -6552,9 +6763,11 @@ function bindsWithoutCollisionIn(bodies, value) {
|
|
|
6552
6763
|
* so facet reachability agrees with the token-boundary {@link matchFacetRecurrence}
|
|
6553
6764
|
* the splice passes use instead of counting a mere substring of a longer token.
|
|
6554
6765
|
*/
|
|
6555
|
-
function occursAtTokenBoundary(
|
|
6556
|
-
if (
|
|
6766
|
+
function occursAtTokenBoundary(rawText, rawValue, ignoreCase = false) {
|
|
6767
|
+
if (rawValue.length === 0)
|
|
6557
6768
|
return false;
|
|
6769
|
+
const text = ignoreCase ? rawText.toLowerCase() : rawText;
|
|
6770
|
+
const value = ignoreCase ? rawValue.toLowerCase() : rawValue;
|
|
6558
6771
|
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
6559
6772
|
let from = 0;
|
|
6560
6773
|
while (true) {
|
|
@@ -6575,22 +6788,45 @@ function occursAtTokenBoundary(text, value) {
|
|
|
6575
6788
|
* deciding from the hash-derived literal alone.
|
|
6576
6789
|
*/
|
|
6577
6790
|
function resolveRecurringNavigateToFacets(declared, actions) {
|
|
6578
|
-
const appearsAnywhere = (value) => actions.some(({ capture }) => occursAtTokenBoundary(capture.url, value) ||
|
|
6791
|
+
const appearsAnywhere = (value) => actions.some(({ capture }) => occursAtTokenBoundary(capture.url, value, true) ||
|
|
6579
6792
|
(capture.requestPostData != null &&
|
|
6580
|
-
occursAtTokenBoundary(capture.requestPostData, value)) ||
|
|
6581
|
-
Object.values(capture.requestHeaders).some((h) => occursAtTokenBoundary(h, value)));
|
|
6793
|
+
occursAtTokenBoundary(capture.requestPostData, value, true)) ||
|
|
6794
|
+
Object.values(capture.requestHeaders).some((h) => occursAtTokenBoundary(h, value, true)));
|
|
6582
6795
|
const reachable = declared.filter(({ value }) => value.length > 0 && appearsAnywhere(value));
|
|
6583
|
-
const resolvedDeclared = declared.filter((
|
|
6584
|
-
const unreachable = declared.filter((facet) => !reachable.includes(facet));
|
|
6585
|
-
|
|
6796
|
+
const resolvedDeclared = declared.filter((facet) => facet.value.length > 0 && (facet.origin !== "step" || reachable.includes(facet)));
|
|
6797
|
+
const unreachable = declared.filter((facet) => facet.origin !== "step" && !reachable.includes(facet));
|
|
6798
|
+
// Every declared facet also owns the elements of its own kind in the code
|
|
6799
|
+
// vocabulary, whether or not its declared literal recurs: a literal that
|
|
6800
|
+
// recurs in one vocabulary (a hash slug, a quoted label) says nothing about
|
|
6801
|
+
// the same facet's element in another (`WW;filterType=ship`).
|
|
6802
|
+
const claimedLiterals = new Set();
|
|
6803
|
+
const aliased = declared.flatMap((facet) => {
|
|
6804
|
+
const literal = findKindKeyedLiteral(facet, actions);
|
|
6805
|
+
if (literal === undefined || declared.some(({ value }) => value === literal))
|
|
6806
|
+
return [];
|
|
6807
|
+
if (claimedLiterals.has(literal))
|
|
6808
|
+
return [];
|
|
6809
|
+
claimedLiterals.add(literal);
|
|
6810
|
+
const known = [facet.value, ...(facet.literals ?? [])];
|
|
6811
|
+
if (known.some((candidate) => candidate.toLowerCase() === literal.toLowerCase()))
|
|
6812
|
+
return [];
|
|
6813
|
+
const valueMap = new Map(facet.value.length > 0 ? [[facet.value, literal]] : []);
|
|
6814
|
+
return [{ ...facet, value: literal, literals: [literal], keyedByKind: true, valueMap }];
|
|
6815
|
+
});
|
|
6816
|
+
if (unreachable.length === 0 && aliased.length === 0)
|
|
6586
6817
|
return resolvedDeclared;
|
|
6587
|
-
const
|
|
6818
|
+
const aliasedFields = new Set(aliased.map(({ field }) => field));
|
|
6819
|
+
const awaiting = unreachable.filter(({ field }) => !aliasedFields.has(field));
|
|
6820
|
+
const withAliases = [...resolvedDeclared, ...aliased];
|
|
6821
|
+
if (awaiting.length === 0)
|
|
6822
|
+
return withAliases;
|
|
6823
|
+
const correlated = correlateUnreachableNavigateToFacets(awaiting, actions, [...reachable, ...aliased].map(({ value }) => value));
|
|
6588
6824
|
if (correlated === null)
|
|
6589
|
-
return
|
|
6825
|
+
return withAliases;
|
|
6590
6826
|
const bodies = actions
|
|
6591
6827
|
.map(({ capture }) => capture.requestPostData)
|
|
6592
6828
|
.filter((body) => typeof body === "string" && body.length > 0);
|
|
6593
|
-
const extras =
|
|
6829
|
+
const extras = awaiting.flatMap((facet) => {
|
|
6594
6830
|
const diffed = correlated.get(facet.field);
|
|
6595
6831
|
if (diffed === undefined || diffed.length === 0)
|
|
6596
6832
|
return [];
|
|
@@ -6598,7 +6834,50 @@ function resolveRecurringNavigateToFacets(declared, actions) {
|
|
|
6598
6834
|
return [];
|
|
6599
6835
|
return bindsWithoutCollisionIn(bodies, diffed) ? [{ ...facet, value: diffed }] : [];
|
|
6600
6836
|
});
|
|
6601
|
-
return [...
|
|
6837
|
+
return [...withAliases, ...extras];
|
|
6838
|
+
}
|
|
6839
|
+
/**
|
|
6840
|
+
* The literal a facet takes in the code vocabulary: the captured request
|
|
6841
|
+
* element (query value or body string leaf) whose `<literal><delim>key=<kind>`
|
|
6842
|
+
* suffix names the facet's own kind. Derived from the traffic itself, so it
|
|
6843
|
+
* never depends on how many other facets or transitions the flow declared.
|
|
6844
|
+
* Returns `undefined` unless exactly one distinct literal carries the kind.
|
|
6845
|
+
*/
|
|
6846
|
+
function findKindKeyedLiteral(facet, actions) {
|
|
6847
|
+
const byTier = new Map();
|
|
6848
|
+
const sameMonth = new Set();
|
|
6849
|
+
const facetMonths = new Set([facet.value, ...(facet.literals ?? [])]
|
|
6850
|
+
.map((literal) => monthCodeOf(literal))
|
|
6851
|
+
.filter((code) => code !== undefined));
|
|
6852
|
+
const yearlessMonths = new Set([facet.value, ...(facet.literals ?? [])]
|
|
6853
|
+
.map((literal) => bareMonthNumberOf(literal))
|
|
6854
|
+
.filter((month) => month !== undefined));
|
|
6855
|
+
for (const { capture } of actions) {
|
|
6856
|
+
const values = [
|
|
6857
|
+
...parseUrlQueryValues(capture.url),
|
|
6858
|
+
...parseBodyLeafValues(capture.requestPostData),
|
|
6859
|
+
];
|
|
6860
|
+
for (const value of values) {
|
|
6861
|
+
const parsed = parseElementKind(value);
|
|
6862
|
+
if (parsed === undefined)
|
|
6863
|
+
continue;
|
|
6864
|
+
const tier = kindMatchTier(facet, parsed.kind);
|
|
6865
|
+
if (tier > 0)
|
|
6866
|
+
byTier.set(tier, (byTier.get(tier) ?? new Set()).add(parsed.literal));
|
|
6867
|
+
const month = monthCodeOf(parsed.literal);
|
|
6868
|
+
const isOwnLiteral = [facet.value, ...(facet.literals ?? [])].some((known) => known.toLowerCase() === parsed.literal.toLowerCase());
|
|
6869
|
+
if (month !== undefined &&
|
|
6870
|
+
!isOwnLiteral &&
|
|
6871
|
+
(facetMonths.has(month) || yearlessMonths.has(month.slice("YYYY-".length)))) {
|
|
6872
|
+
sameMonth.add(parsed.literal);
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
const closest = [...byTier.entries()].sort(([a], [b]) => a - b)[0]?.[1];
|
|
6877
|
+
if (closest !== undefined && closest.size === 1)
|
|
6878
|
+
return [...closest][0];
|
|
6879
|
+
const sameMonthInKind = [...(closest ?? sameMonth)].filter((literal) => sameMonth.has(literal));
|
|
6880
|
+
return sameMonthInKind.length === 1 ? sameMonthInKind[0] : undefined;
|
|
6602
6881
|
}
|
|
6603
6882
|
/**
|
|
6604
6883
|
* Resolves each zero-recurrence navigateTo facet (in flow order) to the real
|
|
@@ -6683,7 +6962,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6683
6962
|
// below can correlate a REST body's facet-packed string leaves against it.
|
|
6684
6963
|
const { payloadFieldNames: restPayloadFieldNames, optionalPayloadFieldNames: restOptionalFieldNames, } = computeFlowPayloadFieldNames(flowSteps, vocabulary, process.env);
|
|
6685
6964
|
const payloadAccessorByValue = new Map();
|
|
6686
|
-
const declaredNavigateToFacets = extractNavigateToFacetOrder(flowSteps);
|
|
6965
|
+
const declaredNavigateToFacets = extractNavigateToFacetOrder(flowSteps, process.env);
|
|
6687
6966
|
const navigateToFacetOrder = resolveRecurringNavigateToFacets(declaredNavigateToFacets, actions);
|
|
6688
6967
|
const facetArrayPathKeys = collectFacetArrayPathKeys(actions.map((action) => parseJsonBodyOrNull(action.capture.requestPostData)), navigateToFacetOrder);
|
|
6689
6968
|
if (inputBody !== undefined && inputBody !== null) {
|
|
@@ -6959,6 +7238,21 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
6959
7238
|
// `foldReturnSpec`, not on Pass 1's render) so Pass 1 knows which values to
|
|
6960
7239
|
// spare per step, mirroring the header-name collection above.
|
|
6961
7240
|
const joinFieldValuesByStep = new Map();
|
|
7241
|
+
// Request bodies of the absorbed repeat captures of each fold target's
|
|
7242
|
+
// representative drill (see FoldPlan.absorbedIndices): never emitted
|
|
7243
|
+
// themselves, so a facet only their differently shaped arrays carry must be
|
|
7244
|
+
// threaded through the representative's body instead of being dropped.
|
|
7245
|
+
const absorbedSiblingBodiesByStep = new Map();
|
|
7246
|
+
for (const plan of earlyFoldPlans) {
|
|
7247
|
+
for (const target of plan.targets) {
|
|
7248
|
+
const drillEndpoint = endpointKey(actions[target.drillStepIndex].capture.url);
|
|
7249
|
+
const siblings = plan.absorbedIndices
|
|
7250
|
+
.map((index) => actions[index].capture)
|
|
7251
|
+
.filter((capture) => endpointKey(capture.url) === drillEndpoint)
|
|
7252
|
+
.map((capture) => parseJsonBodyOrNull(capture.requestPostData));
|
|
7253
|
+
absorbedSiblingBodiesByStep.set(target.drillStepIndex, siblings);
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
6962
7256
|
for (const plan of earlyFoldPlans) {
|
|
6963
7257
|
const primaryItems = objectItemsAtPath(actions[plan.primaryStepIndex].capture.responseBody, plan.primaryArrayPath);
|
|
6964
7258
|
for (const target of plan.targets) {
|
|
@@ -7074,7 +7368,7 @@ flowSteps = [], vocabulary = undefined) {
|
|
|
7074
7368
|
// unparameterized structured value — see
|
|
7075
7369
|
// {@link applyArrayFacetSplicePayloadSubstitutions}.
|
|
7076
7370
|
const rawBodyWithOptionalArrayFacetSplices = parsedBody !== null
|
|
7077
|
-
? applyArrayFacetSplicePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, navigateToFacetOrder, facetArrayPathKeys)
|
|
7371
|
+
? applyArrayFacetSplicePayloadSubstitutions(rawBodyWithFacetSplices, parsedBody, navigateToFacetOrder, facetArrayPathKeys, absorbedSiblingBodiesByStep.get(i))
|
|
7078
7372
|
: rawBodyWithFacetSplices;
|
|
7079
7373
|
const rawBodyWithScalarFacetSplices = parsedBody !== null
|
|
7080
7374
|
? applyScalarFacetSplicePayloadSubstitutions(rawBodyWithOptionalArrayFacetSplices, parsedBody, navigateToFacetOrder)
|
|
@@ -8031,17 +8325,157 @@ function matchArrayElementFacet(element, facets) {
|
|
|
8031
8325
|
*/
|
|
8032
8326
|
function matchFacetRecurrence(text, facets, options) {
|
|
8033
8327
|
const isAlnum = (ch) => ch !== undefined && /[A-Za-z0-9]/.test(ch);
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8328
|
+
const parsedElement = parseElementKind(text);
|
|
8329
|
+
const elementKind = parsedElement?.kind;
|
|
8330
|
+
// A kind no declared facet names (an identifier format such as
|
|
8331
|
+
// `<slug>;filterId=urlFriendlyId`) cannot belong to another facet, so the
|
|
8332
|
+
// kind gate only protects elements some facet claims; a whole-literal
|
|
8333
|
+
// match there still binds the facet whose literal the element carries.
|
|
8334
|
+
const kindUnclaimed = elementKind !== undefined && !facets.some((candidate) => kindsMatch(candidate, elementKind));
|
|
8335
|
+
const pick = (ignoreCase) => facets.reduce((best, facet) => {
|
|
8336
|
+
const readFromElement = facet.keyedByKind === true && parsedElement?.literal === facet.value;
|
|
8337
|
+
const ownsUnclaimedKind = kindUnclaimed &&
|
|
8338
|
+
[facet.value, ...(facet.literals ?? [])].some((known) => known.toLowerCase() === parsedElement?.literal.toLowerCase());
|
|
8339
|
+
if (elementKind !== undefined &&
|
|
8340
|
+
isKindKeyed(facet) &&
|
|
8341
|
+
!readFromElement &&
|
|
8342
|
+
!ownsUnclaimedKind &&
|
|
8343
|
+
!kindsMatch(facet, elementKind)) {
|
|
8344
|
+
return best;
|
|
8345
|
+
}
|
|
8346
|
+
const literal = [facet.value, ...(facet.literals ?? [])]
|
|
8347
|
+
.filter((candidate) => {
|
|
8348
|
+
const same = (value) => ignoreCase ? value.toLowerCase() === candidate.toLowerCase() : value === candidate;
|
|
8349
|
+
return (candidate.length > 0 &&
|
|
8350
|
+
(same(text) ||
|
|
8351
|
+
(options.allowDelimiterSuffix &&
|
|
8352
|
+
same(text.slice(0, candidate.length)) &&
|
|
8353
|
+
!isAlnum(text[candidate.length]))));
|
|
8354
|
+
})
|
|
8355
|
+
.reduce((longest, candidate) => longest === undefined || candidate.length > longest.length ? candidate : longest, undefined);
|
|
8356
|
+
const matchedLiteral = literal;
|
|
8357
|
+
if (matchedLiteral === undefined)
|
|
8042
8358
|
return best;
|
|
8043
|
-
|
|
8359
|
+
const matched = matchedLiteral === facet.value ? facet : { ...facet, value: matchedLiteral };
|
|
8360
|
+
return best === undefined || matchedLiteral.length > best.value.length ? matched : best;
|
|
8044
8361
|
}, undefined);
|
|
8362
|
+
return pick(false) ?? pick(true);
|
|
8363
|
+
}
|
|
8364
|
+
/**
|
|
8365
|
+
* Splits an element of the grammar `<literal><delim><key>=<kind>` into its
|
|
8366
|
+
* literal and kind, or returns `undefined` when the element carries no kind
|
|
8367
|
+
* suffix. Structural only — no key or kind vocabulary is assumed.
|
|
8368
|
+
*/
|
|
8369
|
+
function parseElementKind(element) {
|
|
8370
|
+
const parsed = /^(.+)[;&|,]([^;&|,=]+)=([^;&|,=]+)$/.exec(element);
|
|
8371
|
+
return parsed ? { literal: parsed[1], kind: parsed[3] } : undefined;
|
|
8372
|
+
}
|
|
8373
|
+
function normalizeKind(kind) {
|
|
8374
|
+
return deriveFacetKind(kind).replace(/\s+/g, "").replace(/s$/, "");
|
|
8375
|
+
}
|
|
8376
|
+
const KIND_NOISE_WORDS = new Set(["facet", "filter", "field", "value", "option", "selection"]);
|
|
8377
|
+
/**
|
|
8378
|
+
* How closely a facet names an element's kind, 0 when it does not: 1 for the
|
|
8379
|
+
* declared field (qualifier words such as `facet` dropped) or a hash-named
|
|
8380
|
+
* hint equal to the kind, 2 when the field's trailing word is the kind
|
|
8381
|
+
* (`sailDate` names a `date` element), 3 when any other word of the field is.
|
|
8382
|
+
* A caller picking an element for the facet takes the closest tier, so a field
|
|
8383
|
+
* naming two kinds still resolves to the one it ends with.
|
|
8384
|
+
*/
|
|
8385
|
+
function kindMatchTier(facet, elementKind) {
|
|
8386
|
+
const element = normalizeKind(elementKind);
|
|
8387
|
+
if (element.length === 0 || facet.kind === undefined)
|
|
8388
|
+
return 0;
|
|
8389
|
+
const words = deriveFacetKind(facet.kind).split(" ");
|
|
8390
|
+
const meaningful = words.filter((word) => !KIND_NOISE_WORDS.has(word));
|
|
8391
|
+
const named = meaningful.length > 0 ? meaningful : words;
|
|
8392
|
+
if (normalizeKind(facet.kind) === element || normalizeKind(named.join(" ")) === element)
|
|
8393
|
+
return 1;
|
|
8394
|
+
if ((FACET_KIND_HINTS.get(facet) ?? []).some((hint) => normalizeKind(hint) === element))
|
|
8395
|
+
return 1;
|
|
8396
|
+
if (normalizeKind(named[named.length - 1] ?? "") === element)
|
|
8397
|
+
return 2;
|
|
8398
|
+
return named.some((word) => normalizeKind(word) === element) ? 3 : 0;
|
|
8399
|
+
}
|
|
8400
|
+
function kindsMatch(facet, elementKind) {
|
|
8401
|
+
return kindMatchTier(facet, elementKind) > 0;
|
|
8402
|
+
}
|
|
8403
|
+
const MONTH_NAMES = [
|
|
8404
|
+
"january",
|
|
8405
|
+
"february",
|
|
8406
|
+
"march",
|
|
8407
|
+
"april",
|
|
8408
|
+
"may",
|
|
8409
|
+
"june",
|
|
8410
|
+
"july",
|
|
8411
|
+
"august",
|
|
8412
|
+
"september",
|
|
8413
|
+
"october",
|
|
8414
|
+
"november",
|
|
8415
|
+
"december",
|
|
8416
|
+
];
|
|
8417
|
+
/**
|
|
8418
|
+
* Normalizes a month written as a slug or label (`december-2026`, `Dec 2026`,
|
|
8419
|
+
* `2026-12`) to `YYYY-MM`, so the same month matches across vocabularies.
|
|
8420
|
+
*/
|
|
8421
|
+
function monthCodeOf(literal) {
|
|
8422
|
+
const text = literal.trim();
|
|
8423
|
+
const iso = /^(\d{4})[-/](0[1-9]|1[0-2])$/.exec(text);
|
|
8424
|
+
if (iso)
|
|
8425
|
+
return `${iso[1]}-${iso[2]}`;
|
|
8426
|
+
const named = /^([A-Za-z]{3,9})[\s_/-]+(\d{4})$/.exec(text) ?? /^(\d{4})[\s_/-]+([A-Za-z]{3,9})$/.exec(text);
|
|
8427
|
+
if (!named)
|
|
8428
|
+
return undefined;
|
|
8429
|
+
const yearFirst = /^\d{4}$/.test(named[1]);
|
|
8430
|
+
const name = (yearFirst ? named[2] : named[1]).toLowerCase();
|
|
8431
|
+
const year = yearFirst ? named[1] : named[2];
|
|
8432
|
+
const index = MONTH_NAMES.findIndex((month) => month === name || (name.length === 3 && month.startsWith(name)));
|
|
8433
|
+
return index === -1 ? undefined : `${year}-${String(index + 1).padStart(2, "0")}`;
|
|
8434
|
+
}
|
|
8435
|
+
/** Month number (`12`) of a month written without a year (`december`, `Dec`),
|
|
8436
|
+
* so a facet whose literal names only the month still matches a dated element. */
|
|
8437
|
+
function bareMonthNumberOf(literal) {
|
|
8438
|
+
const name = literal.trim().toLowerCase();
|
|
8439
|
+
const index = MONTH_NAMES.findIndex((month) => month === name || (name.length === 3 && month.startsWith(name)));
|
|
8440
|
+
return index === -1 ? undefined : String(index + 1).padStart(2, "0");
|
|
8441
|
+
}
|
|
8442
|
+
function isKindKeyed(facet) {
|
|
8443
|
+
return (facet.kind !== undefined &&
|
|
8444
|
+
facet.kind.length > 0 &&
|
|
8445
|
+
(facet.origin === "step" || facet.keyedByKind === true));
|
|
8446
|
+
}
|
|
8447
|
+
/**
|
|
8448
|
+
* Renders the payload value for a matched facet in the casing the captured
|
|
8449
|
+
* element carried, so a caller sending the declared literal reproduces the
|
|
8450
|
+
* captured request exactly. An exact-case or mixed-case occurrence is emitted
|
|
8451
|
+
* untouched.
|
|
8452
|
+
*/
|
|
8453
|
+
function facetCaseMethod(captured, facet) {
|
|
8454
|
+
const prefix = captured.slice(0, facet.value.length);
|
|
8455
|
+
if (prefix === facet.value)
|
|
8456
|
+
return undefined;
|
|
8457
|
+
if (prefix === facet.value.toUpperCase())
|
|
8458
|
+
return "toUpperCase";
|
|
8459
|
+
if (prefix === facet.value.toLowerCase())
|
|
8460
|
+
return "toLowerCase";
|
|
8461
|
+
return undefined;
|
|
8462
|
+
}
|
|
8463
|
+
/**
|
|
8464
|
+
* JS expression for the value a facet's payload field carries in the code
|
|
8465
|
+
* vocabulary: a kind-keyed alias translates the caller's declared literal
|
|
8466
|
+
* through its `valueMap`, passing an already-coded value through untouched.
|
|
8467
|
+
*/
|
|
8468
|
+
function facetPayloadExpression(facet, fallback = "") {
|
|
8469
|
+
const raw = `payload.${facet.field}${fallback}`;
|
|
8470
|
+
if (!facet.valueMap || facet.valueMap.size === 0)
|
|
8471
|
+
return raw;
|
|
8472
|
+
const entries = JSON.stringify([...facet.valueMap.entries()]);
|
|
8473
|
+
return `(new Map(${entries}).get(String(payload.${facet.field})) ?? ${raw})`;
|
|
8474
|
+
}
|
|
8475
|
+
function renderFacetPayloadValue(captured, facet) {
|
|
8476
|
+
const method = facetCaseMethod(captured, facet);
|
|
8477
|
+
const expression = facetPayloadExpression(facet);
|
|
8478
|
+
return method ? `\${String(${expression}).${method}()}` : `\${${expression}}`;
|
|
8045
8479
|
}
|
|
8046
8480
|
/**
|
|
8047
8481
|
* Binds a top-level GraphQL variable whose scalar value (or, for a string, any
|
|
@@ -8054,8 +8488,12 @@ function spliceFacetRecurrenceIntoScalarVariable(value, facets) {
|
|
|
8054
8488
|
return null;
|
|
8055
8489
|
if (typeof value === "string" || typeof value === "number") {
|
|
8056
8490
|
const facet = matchFacetRecurrence(String(value), facets, { allowDelimiterSuffix: false });
|
|
8057
|
-
if (facet !== undefined)
|
|
8058
|
-
|
|
8491
|
+
if (facet !== undefined) {
|
|
8492
|
+
const rendered = renderFacetPayloadValue(String(value), facet);
|
|
8493
|
+
return rendered === `\${${facetPayloadExpression(facet)}}`
|
|
8494
|
+
? facetPayloadExpression(facet)
|
|
8495
|
+
: `\`${rendered}\``;
|
|
8496
|
+
}
|
|
8059
8497
|
}
|
|
8060
8498
|
if (typeof value !== "string")
|
|
8061
8499
|
return null;
|
|
@@ -8077,7 +8515,7 @@ function spliceFacetRecurrenceIntoScalarVariable(value, facets) {
|
|
|
8077
8515
|
continue;
|
|
8078
8516
|
}
|
|
8079
8517
|
bound = true;
|
|
8080
|
-
const unit = `\`${escapeForTemplateLiteral(segment.slice(0, colonIndex + 1))}
|
|
8518
|
+
const unit = `\`${escapeForTemplateLiteral(segment.slice(0, colonIndex + 1))}${renderFacetPayloadValue(segment.slice(colonIndex + 1), facet)}${escapeForTemplateLiteral(trailingDelimiter)}\``;
|
|
8081
8519
|
// An absent optional facet drops its own segment (and delimiter) rather
|
|
8082
8520
|
// than rendering the string `undefined` into the packed value.
|
|
8083
8521
|
if (facet.optional) {
|
|
@@ -8120,7 +8558,7 @@ function spliceFacetsIntoArrayVariable(value, facets, renderElement = (element)
|
|
|
8120
8558
|
return renderElement(element);
|
|
8121
8559
|
matchCount++;
|
|
8122
8560
|
const suffix = element.slice(matched.value.length);
|
|
8123
|
-
const elementLiteral =
|
|
8561
|
+
const elementLiteral = `\`${renderFacetPayloadValue(element, matched)}${escapeForTemplateLiteral(suffix)}\``;
|
|
8124
8562
|
return matched.optional
|
|
8125
8563
|
? `...(payload.${matched.field} ? [${elementLiteral}] : [])`
|
|
8126
8564
|
: elementLiteral;
|
|
@@ -8506,7 +8944,7 @@ function collectRequestBodyValuesByKey(capture, allCaptures) {
|
|
|
8506
8944
|
const otherBody = parsedRequestBodyFor(c);
|
|
8507
8945
|
if (otherBody === undefined)
|
|
8508
8946
|
return undefined;
|
|
8509
|
-
const otherValue =
|
|
8947
|
+
const otherValue = readComparableBodyLeaf(parsedBody, otherBody, path);
|
|
8510
8948
|
return otherValue === undefined ? undefined : String(otherValue);
|
|
8511
8949
|
}
|
|
8512
8950
|
catch {
|
|
@@ -8552,29 +8990,36 @@ function collectRequestStringValues(capture, allCaptures) {
|
|
|
8552
8990
|
*/
|
|
8553
8991
|
function isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, siblingPaths) {
|
|
8554
8992
|
const allBodies = [parsedBody, ...otherBodies];
|
|
8555
|
-
|
|
8556
|
-
|
|
8993
|
+
const siblingReaders = [
|
|
8994
|
+
...[...siblingPaths].map((siblingPath) => {
|
|
8995
|
+
const pathSegments = siblingPath.split(".");
|
|
8996
|
+
return (body) => {
|
|
8997
|
+
const siblingRaw = readComparableBodyLeaf(parsedBody, body, pathSegments);
|
|
8998
|
+
return siblingRaw === undefined ? undefined : String(siblingRaw);
|
|
8999
|
+
};
|
|
9000
|
+
}),
|
|
9001
|
+
...selfDescribingArrayPaths(parsedBody).map((arrayPath) => (body) => {
|
|
9002
|
+
const array = readValueAtPath(body, arrayPath);
|
|
9003
|
+
return Array.isArray(array) ? identityShape(array) : ABSENT_ARRAY_SHAPE;
|
|
9004
|
+
}),
|
|
9005
|
+
];
|
|
9006
|
+
return siblingReaders.some((readSibling) => {
|
|
8557
9007
|
const pairs = [];
|
|
8558
|
-
let sawUnexplainableField = false;
|
|
8559
9008
|
for (const body of allBodies) {
|
|
8560
|
-
const fieldRaw =
|
|
9009
|
+
const fieldRaw = readComparableBodyLeaf(parsedBody, body, path);
|
|
8561
9010
|
if (fieldRaw === undefined)
|
|
8562
9011
|
continue;
|
|
8563
|
-
const
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
}
|
|
8571
|
-
pairs.push({ siblingValue: String(siblingRaw), fieldValue: String(fieldRaw) });
|
|
9012
|
+
const siblingValue = readSibling(body);
|
|
9013
|
+
// This body has a value for the field we're trying to explain but no
|
|
9014
|
+
// value for the candidate sibling — the sibling can't vouch for it, so
|
|
9015
|
+
// don't let a function fitted over the OTHER bodies paper over it.
|
|
9016
|
+
if (siblingValue === undefined)
|
|
9017
|
+
return false;
|
|
9018
|
+
pairs.push({ siblingValue, fieldValue: String(fieldRaw) });
|
|
8572
9019
|
}
|
|
8573
|
-
if (sawUnexplainableField)
|
|
8574
|
-
continue;
|
|
8575
9020
|
const distinctSiblingValues = new Set(pairs.map((p) => p.siblingValue));
|
|
8576
9021
|
if (distinctSiblingValues.size < 2)
|
|
8577
|
-
|
|
9022
|
+
return false;
|
|
8578
9023
|
const siblingToField = new Map();
|
|
8579
9024
|
const isFunction = pairs.every(({ siblingValue, fieldValue }) => {
|
|
8580
9025
|
const existing = siblingToField.get(siblingValue);
|
|
@@ -8584,10 +9029,8 @@ function isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, sibl
|
|
|
8584
9029
|
}
|
|
8585
9030
|
return existing === fieldValue;
|
|
8586
9031
|
});
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
}
|
|
8590
|
-
return false;
|
|
9032
|
+
return isFunction && siblingToField.size === distinctSiblingValues.size;
|
|
9033
|
+
});
|
|
8591
9034
|
}
|
|
8592
9035
|
/**
|
|
8593
9036
|
* Finds every query param or JSON body leaf on `capture`'s own request that
|
|
@@ -8671,7 +9114,7 @@ function findFrozenVaryingDrillParams(capture, renderedText, allCaptures) {
|
|
|
8671
9114
|
continue;
|
|
8672
9115
|
const fieldPath = path.join(".");
|
|
8673
9116
|
const differing = otherBodies
|
|
8674
|
-
.map((otherBody) =>
|
|
9117
|
+
.map((otherBody) => readComparableBodyLeaf(parsedBody, otherBody, path))
|
|
8675
9118
|
.find((v) => v !== undefined && String(v) !== stringValue);
|
|
8676
9119
|
if (differing !== undefined &&
|
|
8677
9120
|
!isExplainedByCorrelatedSiblingField(path, parsedBody, otherBodies, new Set(allPaths.filter((p) => p !== fieldPath)))) {
|
|
@@ -9570,6 +10013,23 @@ function parseFoldReturnSpec(flowFileContents) {
|
|
|
9570
10013
|
return null;
|
|
9571
10014
|
}
|
|
9572
10015
|
}
|
|
10016
|
+
/**
|
|
10017
|
+
* Reads the flow file's `browserFlowOnly: true` opt-in, which forces the
|
|
10018
|
+
* browser-flow-only emit for a submission flow whose HTTP analysis would
|
|
10019
|
+
* otherwise produce an executeHttp path. Anything but a literal `true` (or an
|
|
10020
|
+
* unreadable/legacy bare-array file) is "not opted in".
|
|
10021
|
+
*/
|
|
10022
|
+
function parseBrowserFlowOnlyOptIn(flowFileContents) {
|
|
10023
|
+
try {
|
|
10024
|
+
const raw = JSON.parse(flowFileContents);
|
|
10025
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
10026
|
+
return false;
|
|
10027
|
+
return raw.browserFlowOnly === true;
|
|
10028
|
+
}
|
|
10029
|
+
catch {
|
|
10030
|
+
return false;
|
|
10031
|
+
}
|
|
10032
|
+
}
|
|
9573
10033
|
/**
|
|
9574
10034
|
* Parses `browserFallbackGate`/`httpTimeoutMs` out of an object-form
|
|
9575
10035
|
* recon-flow.json, mirroring {@link parseFoldReturnSpec}'s null-safe pattern:
|
|
@@ -9649,6 +10109,108 @@ function readValueAtPath(body, path) {
|
|
|
9649
10109
|
return node[segment];
|
|
9650
10110
|
}, body);
|
|
9651
10111
|
}
|
|
10112
|
+
const ARRAY_ELEMENT_IDENTITY = /^[\s\S]*([^A-Za-z0-9=]\w+=[\s\S]*)$/;
|
|
10113
|
+
/** The `<delimiter><name>=<anything>` suffix of a self-describing array element
|
|
10114
|
+
* (the grammar {@link matchArrayElementFacet} binds on), or `undefined`. The
|
|
10115
|
+
* suffix is open-ended because slugs and names carry hyphens, dots and colons
|
|
10116
|
+
* — a stricter shape would silently fall back to index comparison for them. */
|
|
10117
|
+
function arrayElementIdentity(element) {
|
|
10118
|
+
if (typeof element !== "string")
|
|
10119
|
+
return undefined;
|
|
10120
|
+
return ARRAY_ELEMENT_IDENTITY.exec(element)?.[1];
|
|
10121
|
+
}
|
|
10122
|
+
/** The `<delimiter><name>` head of an {@link arrayElementIdentity} (its tag
|
|
10123
|
+
* dropped), the part that says what ROLE an element plays in its array. */
|
|
10124
|
+
function identityName(identity) {
|
|
10125
|
+
return identity?.slice(0, identity.indexOf("="));
|
|
10126
|
+
}
|
|
10127
|
+
/** Order-insensitive fingerprint of the identity suffixes an array carries —
|
|
10128
|
+
* two arrays with equal fingerprints are the same shape (reordered, or with
|
|
10129
|
+
* only values changed), anything else is a different shape or length. */
|
|
10130
|
+
function identityShape(elements) {
|
|
10131
|
+
return elements
|
|
10132
|
+
.map((element) => arrayElementIdentity(element) ?? "")
|
|
10133
|
+
.sort()
|
|
10134
|
+
.join("\u0000");
|
|
10135
|
+
}
|
|
10136
|
+
const ABSENT_ARRAY_SHAPE = "\u0001absent";
|
|
10137
|
+
/** Paths of every array in `body` that lists self-describing string elements
|
|
10138
|
+
* (see {@link arrayElementIdentity}). Which identity suffixes such an array
|
|
10139
|
+
* carries is itself a property of the request — a sibling field that differs
|
|
10140
|
+
* only because the array took a different shape is explained by that shape. */
|
|
10141
|
+
function selfDescribingArrayPaths(body, path = []) {
|
|
10142
|
+
if (body === null || typeof body !== "object")
|
|
10143
|
+
return [];
|
|
10144
|
+
const children = Object.entries(body).flatMap(([key, child]) => selfDescribingArrayPaths(child, [...path, key]));
|
|
10145
|
+
const isSelfDescribing = Array.isArray(body) && body.some((element) => arrayElementIdentity(element) !== undefined);
|
|
10146
|
+
return isSelfDescribing ? [[...path], ...children] : children;
|
|
10147
|
+
}
|
|
10148
|
+
/**
|
|
10149
|
+
* Index of the innermost path segment that indexes into an array (the array
|
|
10150
|
+
* holding the leaf, or one holding an object element that carries it), or -1.
|
|
10151
|
+
*/
|
|
10152
|
+
function innermostArrayIndexSegment(body, path) {
|
|
10153
|
+
for (let index = path.length - 1; index >= 0; index--) {
|
|
10154
|
+
if (!/^\d+$/.test(path[index]))
|
|
10155
|
+
continue;
|
|
10156
|
+
if (Array.isArray(readValueAtPath(body, path.slice(0, index))))
|
|
10157
|
+
return index;
|
|
10158
|
+
}
|
|
10159
|
+
return -1;
|
|
10160
|
+
}
|
|
10161
|
+
/**
|
|
10162
|
+
* Counterpart of the primitive leaf at `path` in `body`, read from `otherBody`.
|
|
10163
|
+
* A string leaf inside an array of self-describing elements (directly, or as a
|
|
10164
|
+
* field of an object element) is matched by its identity suffix within the same
|
|
10165
|
+
* array, never by index, so differently shaped arrays are not mistaken for one
|
|
10166
|
+
* array with a varying slot; an identity the other body lacks yields
|
|
10167
|
+
* `undefined`. An identity shared by several elements of either array (e.g.
|
|
10168
|
+
* repeated `;tag=x` suffixes) is ambiguous and is paired only by exact value.
|
|
10169
|
+
* When the arrays differ in shape or length, an identity is trusted only if
|
|
10170
|
+
* its `;name=` role is also unique in both: a tag repeated by position
|
|
10171
|
+
* (`;tagId=t0`, `;tagId=t1`) in two unrelated arrays says nothing about the
|
|
10172
|
+
* elements being the same one.
|
|
10173
|
+
* A plain element of such an array is matched by value, since its
|
|
10174
|
+
* slot says nothing about it once the arrays' shapes differ. Every other path
|
|
10175
|
+
* defers to {@link readValueAtPath}.
|
|
10176
|
+
*/
|
|
10177
|
+
function readComparableBodyLeaf(body, otherBody, path) {
|
|
10178
|
+
const leaf = readValueAtPath(body, path);
|
|
10179
|
+
const arrayEnd = typeof leaf === "string" ? innermostArrayIndexSegment(body, path) : -1;
|
|
10180
|
+
if (arrayEnd === -1)
|
|
10181
|
+
return readValueAtPath(otherBody, path);
|
|
10182
|
+
const arrayPath = path.slice(0, arrayEnd);
|
|
10183
|
+
const elementSubpath = path.slice(arrayEnd + 1);
|
|
10184
|
+
const ownArray = readValueAtPath(body, arrayPath);
|
|
10185
|
+
const otherArray = readValueAtPath(otherBody, arrayPath);
|
|
10186
|
+
const identity = arrayElementIdentity(leaf);
|
|
10187
|
+
const isSelfDescribing = (array) => array.some((element) => arrayElementIdentity(readValueAtPath(element, elementSubpath)));
|
|
10188
|
+
if (identity === undefined &&
|
|
10189
|
+
!isSelfDescribing(ownArray) &&
|
|
10190
|
+
!(Array.isArray(otherArray) && isSelfDescribing(otherArray))) {
|
|
10191
|
+
return readValueAtPath(otherBody, path);
|
|
10192
|
+
}
|
|
10193
|
+
if (!Array.isArray(otherArray))
|
|
10194
|
+
return undefined;
|
|
10195
|
+
const counterparts = otherArray.map((element) => readValueAtPath(element, elementSubpath));
|
|
10196
|
+
if (counterparts.includes(leaf))
|
|
10197
|
+
return leaf;
|
|
10198
|
+
if (identity === undefined)
|
|
10199
|
+
return undefined;
|
|
10200
|
+
const ownCounterparts = ownArray.map((element) => readValueAtPath(element, elementSubpath));
|
|
10201
|
+
const sharesIdentity = (candidate) => arrayElementIdentity(candidate) === identity;
|
|
10202
|
+
const ownSharing = ownCounterparts.filter(sharesIdentity);
|
|
10203
|
+
const otherSharing = counterparts.filter(sharesIdentity);
|
|
10204
|
+
if (ownSharing.length !== 1 || otherSharing.length !== 1)
|
|
10205
|
+
return undefined;
|
|
10206
|
+
const name = identityName(identity);
|
|
10207
|
+
const nameIsUnique = (values) => values.filter((value) => identityName(arrayElementIdentity(value)) === name).length === 1;
|
|
10208
|
+
const isSameShape = identityShape(ownCounterparts) === identityShape(counterparts);
|
|
10209
|
+
if (!isSameShape && !(nameIsUnique(ownCounterparts) && nameIsUnique(counterparts))) {
|
|
10210
|
+
return undefined;
|
|
10211
|
+
}
|
|
10212
|
+
return otherSharing[0];
|
|
10213
|
+
}
|
|
9652
10214
|
/**
|
|
9653
10215
|
* Turns a flow author's plain dot-separated {@link FoldReturnSpec.resultsPath}
|
|
9654
10216
|
* / `drillResultsPath` into the SAME {@link ARRAY_WILDCARD_SEGMENT}-bearing
|
|
@@ -11231,10 +11793,48 @@ function declaredSchemaCoversAccessorShape(declared, shape) {
|
|
|
11231
11793
|
const childShapes = [...(shape.element ? [shape.element] : []), ...shape.keys.values()];
|
|
11232
11794
|
return ([...shape.keys.keys()].every((key) => new RegExp(`(?:^|[\\s{,])"?${key.replace(/[$]/g, "\\$&")}"?\\s*:`).test(declared)) && childShapes.every((child) => declaredSchemaCoversAccessorShape(declared, child)));
|
|
11233
11795
|
}
|
|
11796
|
+
const applicantContactFieldNames = new Set([
|
|
11797
|
+
"FirstName",
|
|
11798
|
+
"LastName",
|
|
11799
|
+
"Phone",
|
|
11800
|
+
"AddressLine",
|
|
11801
|
+
"City",
|
|
11802
|
+
"State",
|
|
11803
|
+
"PostalCode",
|
|
11804
|
+
"Country",
|
|
11805
|
+
"County",
|
|
11806
|
+
"Resume",
|
|
11807
|
+
"ResumeContentType",
|
|
11808
|
+
"ResumeFilename",
|
|
11809
|
+
"ResumeBase64",
|
|
11810
|
+
]);
|
|
11811
|
+
const baseContractFieldNames = new Set(["Email", "ClickUrl", "Answers"]);
|
|
11812
|
+
const applicantContactEvidenceFieldNamesLower = new Set([...applicantContactFieldNames, ...baseContractFieldNames].map((name) => name.toLowerCase()));
|
|
11813
|
+
/**
|
|
11814
|
+
* Single source of truth for "the contract extends ApplicantContactSchema", so
|
|
11815
|
+
* the contract and browser-flow emitters can't disagree about whether the
|
|
11816
|
+
* payload carries Resume fields.
|
|
11817
|
+
*/
|
|
11818
|
+
function contractUsesApplicantContactSchema(opts) {
|
|
11819
|
+
const { inputBody, isSubmissionFlow, browserFlowOnly, payloadFieldNames } = opts;
|
|
11820
|
+
// A browser-flow-only submission flow has no HTTP body of its own: whatever
|
|
11821
|
+
// inputBody HTTP analysis captured describes a path the plugin won't emit,
|
|
11822
|
+
// so it is not evidence about the real caller's shape.
|
|
11823
|
+
if (isSubmissionFlow === true && browserFlowOnly === true)
|
|
11824
|
+
return true;
|
|
11825
|
+
// A body-less submission flow has no inputBody to inspect; the payload fields
|
|
11826
|
+
// its steps splice are the only evidence, whether or not it also has an HTTP
|
|
11827
|
+
// path (the browser-flow emitter can't know which the contract will take).
|
|
11828
|
+
if (inputBody != null) {
|
|
11829
|
+
return [...walkAllPrimitiveLeaves(inputBody)].some(({ path }) => path.some((segment) => applicantContactEvidenceFieldNamesLower.has(segment.toLowerCase())));
|
|
11830
|
+
}
|
|
11831
|
+
return (isSubmissionFlow === true &&
|
|
11832
|
+
[...(payloadFieldNames ?? [])].some((name) => applicantContactEvidenceFieldNamesLower.has(name.toLowerCase())));
|
|
11833
|
+
}
|
|
11234
11834
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
11235
11835
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
11236
11836
|
function emitContractTs(opts) {
|
|
11237
|
-
const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, valueConstraints = value_constraints_1.EMPTY_VALUE_CONSTRAINTS, payloadFieldNames, optionalPayloadFieldNames = new Set(), flowSteps: contractFlowSteps = [], headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
|
|
11837
|
+
const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, createsAccount = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, valueConstraints = value_constraints_1.EMPTY_VALUE_CONSTRAINTS, payloadFieldNames, optionalPayloadFieldNames = new Set(), flowSteps: contractFlowSteps = [], headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
|
|
11238
11838
|
const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
|
|
11239
11839
|
/** Single source of truth for every GraphQL-emission decision below
|
|
11240
11840
|
* (clientImport, gqlCacheBlock, queryConst, and both executeHttpBody
|
|
@@ -11311,25 +11911,12 @@ function emitContractTs(opts) {
|
|
|
11311
11911
|
// one of ApplicantContactSchema's own field names (case-insensitively,
|
|
11312
11912
|
// checked at any depth via walkAllPrimitiveLeaves) before the template
|
|
11313
11913
|
// applies (see recon-generate-payload-schema-mismatch.md).
|
|
11314
|
-
const
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
"State",
|
|
11321
|
-
"PostalCode",
|
|
11322
|
-
"Country",
|
|
11323
|
-
"County",
|
|
11324
|
-
"Resume",
|
|
11325
|
-
"ResumeContentType",
|
|
11326
|
-
"ResumeFilename",
|
|
11327
|
-
"ResumeBase64",
|
|
11328
|
-
]);
|
|
11329
|
-
const baseContractFieldNames = new Set(["Email", "ClickUrl", "Answers"]);
|
|
11330
|
-
const applicantContactEvidenceFieldNamesLower = new Set([...applicantContactFieldNames, ...baseContractFieldNames].map((name) => name.toLowerCase()));
|
|
11331
|
-
const usesApplicantContactSchema = inputBody != null &&
|
|
11332
|
-
[...walkAllPrimitiveLeaves(inputBody)].some(({ path }) => path.some((segment) => applicantContactEvidenceFieldNamesLower.has(segment.toLowerCase())));
|
|
11914
|
+
const usesApplicantContactSchema = contractUsesApplicantContactSchema({
|
|
11915
|
+
inputBody,
|
|
11916
|
+
isSubmissionFlow,
|
|
11917
|
+
browserFlowOnly: omitExecuteHttp,
|
|
11918
|
+
payloadFieldNames,
|
|
11919
|
+
});
|
|
11333
11920
|
const isReservedByApplicantContactSchema = (name) => usesApplicantContactSchema && applicantContactFieldNames.has(name);
|
|
11334
11921
|
// A missing inputBody, or one with no ApplicantContactSchema evidence,
|
|
11335
11922
|
// means this is not a job-application submission flow. The former keeps
|
|
@@ -11339,7 +11926,7 @@ function emitContractTs(opts) {
|
|
|
11339
11926
|
// the other sources unchanged.
|
|
11340
11927
|
const basePayloadSchemaExpr = usesApplicantContactSchema
|
|
11341
11928
|
? `ApplicantContactSchema`
|
|
11342
|
-
: inputBody
|
|
11929
|
+
: inputBody || (omitExecuteHttp && isSubmissionFlow)
|
|
11343
11930
|
? `z.object({})`
|
|
11344
11931
|
: `z.object({\n query: z.string().min(1),\n})`;
|
|
11345
11932
|
// Only the single-endpoint read path (a real primary operation, no
|
|
@@ -11455,11 +12042,16 @@ function emitContractTs(opts) {
|
|
|
11455
12042
|
// as required strings (z.email() for Email per the repo's z.string().email()→
|
|
11456
12043
|
// z.email() migration) so those references typecheck in the generated flow.
|
|
11457
12044
|
if (payloadFieldNames) {
|
|
11458
|
-
|
|
12045
|
+
// The browser-flow-only emit canonicalises its own splices already; this
|
|
12046
|
+
// covers a caller handing over vocabulary-row names directly, so one never
|
|
12047
|
+
// becomes a required key beside the base schema's own equivalent.
|
|
12048
|
+
const canonicaliseNames = usesApplicantContactSchema && omitExecuteHttp && isSubmissionFlow;
|
|
12049
|
+
for (const rawName of [...payloadFieldNames].sort()) {
|
|
12050
|
+
const name = canonicaliseNames ? canonicalPayloadField(rawName) : rawName;
|
|
11459
12051
|
if (isReservedByApplicantContactSchema(name))
|
|
11460
12052
|
continue;
|
|
11461
12053
|
const zod = name === "Email" ? "z.email()" : "z.string()";
|
|
11462
|
-
const suffix = optionalPayloadFieldNames.has(
|
|
12054
|
+
const suffix = optionalPayloadFieldNames.has(rawName) ? ".optional()" : "";
|
|
11463
12055
|
addExtendField(name, ` ${name}: ${zod}${suffix},`);
|
|
11464
12056
|
}
|
|
11465
12057
|
}
|
|
@@ -11670,7 +12262,12 @@ function emitContractTs(opts) {
|
|
|
11670
12262
|
extendFields.set(fieldName, ` ${key}: ${zod}${optional ? ".optional()" : ""},`);
|
|
11671
12263
|
}
|
|
11672
12264
|
const mergedExtension = extendFields.size > 0 ? `.extend({\n${[...extendFields.values()].join("\n")}\n})` : "";
|
|
11673
|
-
|
|
12265
|
+
// zod strips unknown keys, so without composing it a job-application
|
|
12266
|
+
// contract drops the TrackingUrl the engine's fireTrackingClick reads. merge
|
|
12267
|
+
// rather than a second extend keeps the schema to a single `.extend()`.
|
|
12268
|
+
const composesJobTracking = usesApplicantContactSchema && !extendFields.has("TrackingUrl");
|
|
12269
|
+
const trackingMerge = composesJobTracking ? ".merge(JobTrackingSchema)" : "";
|
|
12270
|
+
const payloadSchemaExpr = `${basePayloadSchemaExpr}${mergedExtension}${trackingMerge}`;
|
|
11674
12271
|
// basePayloadSchemaExpr's own Answers field always wraps in
|
|
11675
12272
|
// multipartJsonObject() for job-application submission flows
|
|
11676
12273
|
// (usesApplicantContactSchema); multipartBoolean() is only imported when a
|
|
@@ -11690,6 +12287,9 @@ function emitContractTs(opts) {
|
|
|
11690
12287
|
// ApplicantContactSchema backs the job-application submission-flow payload
|
|
11691
12288
|
// schema (see basePayloadSchemaExpr above); only referenced when
|
|
11692
12289
|
// usesApplicantContactSchema is true.
|
|
12290
|
+
const jobTrackingImport = composesJobTracking
|
|
12291
|
+
? `import { JobTrackingSchema } from "${ENGINE_PKG}/lib/job-tracking";\n`
|
|
12292
|
+
: "";
|
|
11693
12293
|
const applicantContactImport = usesApplicantContactSchema
|
|
11694
12294
|
? `import { ApplicantContactSchema } from "${ENGINE_PKG}/lib/applicant-payload";\n`
|
|
11695
12295
|
: "";
|
|
@@ -11770,7 +12370,9 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
11770
12370
|
// the same captured request-variables object the signal was itself
|
|
11771
12371
|
// detected from (see paginationOperationIdentity above) — the default
|
|
11772
12372
|
// `{ q: payload.query }` REST body has no skip/count container to advance.
|
|
11773
|
-
|
|
12373
|
+
// A single-primary flow compiles no action steps, so its captured traffic
|
|
12374
|
+
// is the only place a declared facet's kind-suffixed element can be found.
|
|
12375
|
+
const navigateToFacets = resolveRecurringNavigateToFacets(extractNavigateToFacetOrder(contractFlowSteps, process.env), actionSteps.length > 0 ? actionSteps : allCaptures.map((capture) => ({ capture })));
|
|
11774
12376
|
const gqlVariablesExpr = gqlOperationName
|
|
11775
12377
|
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames, navigateToFacets)
|
|
11776
12378
|
: paginationSignal
|
|
@@ -12158,6 +12760,19 @@ const limiter = new Bottleneck({ minTime: ${minTime} });
|
|
|
12158
12760
|
${executeHttpBody}
|
|
12159
12761
|
},
|
|
12160
12762
|
`;
|
|
12763
|
+
// Applicant contracts enter on payload.ClickUrl, so a browser-flow-only
|
|
12764
|
+
// execute has nothing left to read from the context.
|
|
12765
|
+
const executeOmitsContext = omitExecuteHttp && usesApplicantContactSchema;
|
|
12766
|
+
const executeContextParam = executeOmitsContext ? "" : `,\n context: SitePluginContext`;
|
|
12767
|
+
const sitePluginImports = executeOmitsContext
|
|
12768
|
+
? "SitePlugin, SitePluginResult"
|
|
12769
|
+
: "SitePlugin, SitePluginContext, SitePluginResult";
|
|
12770
|
+
const executeDocComment = omitExecuteHttp
|
|
12771
|
+
? ""
|
|
12772
|
+
: ` /** Browser fallback: Stagehand + Steel — invoked only when hot path fails. */\n`;
|
|
12773
|
+
const maxAttemptsLiteral = createsAccount
|
|
12774
|
+
? `\n // A retry would re-register the same account the first attempt created.\n maxAttempts: 1,`
|
|
12775
|
+
: "";
|
|
12161
12776
|
const pluginDocComment = omitExecuteHttp
|
|
12162
12777
|
? `/**
|
|
12163
12778
|
* Plugin for ${siteId}. Browser-flow-only: the captured multi-step submission
|
|
@@ -12175,9 +12790,9 @@ ${executeHttpBody}
|
|
|
12175
12790
|
|
|
12176
12791
|
${bottleneckImport}import { z } from "zod/v4";
|
|
12177
12792
|
|
|
12178
|
-
${fixtureImport}${applicantContactImport}${caseInsensitiveHeadersImport}${mergeFoldedPrimaryBodiesImport}${multipartBoolImport}${clientImport}
|
|
12793
|
+
${fixtureImport}${applicantContactImport}${jobTrackingImport}${caseInsensitiveHeadersImport}${mergeFoldedPrimaryBodiesImport}${multipartBoolImport}${clientImport}
|
|
12179
12794
|
import type { BrowserSession } from "${ENGINE_PKG}/scraper/session";
|
|
12180
|
-
import type {
|
|
12795
|
+
import type { ${sitePluginImports} } from "${ENGINE_PKG}/site-plugin";
|
|
12181
12796
|
import { run${pascal}BrowserFlow } from "@/sites/${siteId}/flows/browser-flow";
|
|
12182
12797
|
${baseHeadersBlock}${limiterBlock}
|
|
12183
12798
|
export const ${pascal}ResponseSchema = ${responseSchemaExpr};
|
|
@@ -12207,14 +12822,12 @@ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
|
12207
12822
|
// since the multipart wire format is what makes that field's
|
|
12208
12823
|
// JSON-stringified encoding parseable.
|
|
12209
12824
|
`
|
|
12210
|
-
: ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}${browserFallbackGateLiteral}
|
|
12825
|
+
: ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}${browserFallbackGateLiteral}${maxAttemptsLiteral}
|
|
12211
12826
|
},
|
|
12212
12827
|
${executeHttpMethodBlock}
|
|
12213
|
-
|
|
12214
|
-
async execute(
|
|
12828
|
+
${executeDocComment} async execute(
|
|
12215
12829
|
payload: ${pascal}Payload,
|
|
12216
|
-
session: BrowserSession
|
|
12217
|
-
context: SitePluginContext
|
|
12830
|
+
session: BrowserSession${executeContextParam}
|
|
12218
12831
|
): Promise<SitePluginResult<${pascal}Response>> {
|
|
12219
12832
|
const raw = await run${pascal}BrowserFlow(session.stagehand, ${usesApplicantContactSchema ? "payload.ClickUrl" : "context.baseUrl"}, payload, session.sessionProxy ?? null);
|
|
12220
12833
|
return { data: raw as ${pascal}Response };
|
|
@@ -12259,6 +12872,101 @@ function assertRequiredUrlFieldsReferenced(contractCode, browserFlowCode) {
|
|
|
12259
12872
|
throw new Error(`recon-generate: required URL field(s) ${unreferenced.join(", ")} declared on the payload schema ` +
|
|
12260
12873
|
`but never referenced by the emitted contract or browser flow — the flow would enter on the wrong page`);
|
|
12261
12874
|
}
|
|
12875
|
+
/**
|
|
12876
|
+
* Blank comments and plain string literals so a `payload.<name>` mention inside
|
|
12877
|
+
* prose or an unrelated string is not mistaken for a read. Template-literal
|
|
12878
|
+
* text is blanked but its `${…}` expressions are kept, because the browser flow
|
|
12879
|
+
* splices fields as `${payload.X}`.
|
|
12880
|
+
*/
|
|
12881
|
+
function stripNonCodeText(code) {
|
|
12882
|
+
const out = [];
|
|
12883
|
+
// Non-negative: brace depth at which a template literal's text resumes; negative: -(depth+1) of an open `${`.
|
|
12884
|
+
const templateDepths = [];
|
|
12885
|
+
let depth = 0;
|
|
12886
|
+
let i = 0;
|
|
12887
|
+
while (i < code.length) {
|
|
12888
|
+
const ch = code[i];
|
|
12889
|
+
const next = code[i + 1];
|
|
12890
|
+
const inTemplateText = templateDepths.length > 0 && templateDepths[templateDepths.length - 1] === depth;
|
|
12891
|
+
if (inTemplateText) {
|
|
12892
|
+
if (ch === "\\")
|
|
12893
|
+
i += 2;
|
|
12894
|
+
else if (ch === "`") {
|
|
12895
|
+
templateDepths.pop();
|
|
12896
|
+
i++;
|
|
12897
|
+
}
|
|
12898
|
+
else if (ch === "$" && next === "{") {
|
|
12899
|
+
templateDepths.push(-(depth + 1));
|
|
12900
|
+
depth++;
|
|
12901
|
+
out.push("{");
|
|
12902
|
+
i += 2;
|
|
12903
|
+
}
|
|
12904
|
+
else
|
|
12905
|
+
i++;
|
|
12906
|
+
continue;
|
|
12907
|
+
}
|
|
12908
|
+
if (ch === "/" && next === "/") {
|
|
12909
|
+
while (i < code.length && code[i] !== "\n")
|
|
12910
|
+
i++;
|
|
12911
|
+
}
|
|
12912
|
+
else if (ch === "/" && next === "*") {
|
|
12913
|
+
const end = code.indexOf("*/", i + 2);
|
|
12914
|
+
i = end === -1 ? code.length : end + 2;
|
|
12915
|
+
}
|
|
12916
|
+
else if (ch === '"' || ch === "'") {
|
|
12917
|
+
i++;
|
|
12918
|
+
while (i < code.length && code[i] !== ch && code[i] !== "\n")
|
|
12919
|
+
i += code[i] === "\\" ? 2 : 1;
|
|
12920
|
+
i++;
|
|
12921
|
+
out.push('""');
|
|
12922
|
+
}
|
|
12923
|
+
else if (ch === "`") {
|
|
12924
|
+
templateDepths.push(depth);
|
|
12925
|
+
i++;
|
|
12926
|
+
}
|
|
12927
|
+
else {
|
|
12928
|
+
if (ch === "{")
|
|
12929
|
+
depth++;
|
|
12930
|
+
if (ch === "}") {
|
|
12931
|
+
depth--;
|
|
12932
|
+
if (templateDepths[templateDepths.length - 1] === -(depth + 1))
|
|
12933
|
+
templateDepths.pop();
|
|
12934
|
+
}
|
|
12935
|
+
out.push(ch ?? "");
|
|
12936
|
+
i++;
|
|
12937
|
+
}
|
|
12938
|
+
}
|
|
12939
|
+
return out.join("");
|
|
12940
|
+
}
|
|
12941
|
+
/**
|
|
12942
|
+
* Names of the payload fields a flow explicitly declared via `payloadField`
|
|
12943
|
+
* that no emitted request surface reads as `payload.<field>`. The surface is
|
|
12944
|
+
* the emitted contract plus the browser flow: a field only the browser flow
|
|
12945
|
+
* dereferences still changes browser-executed requests, so it counts as
|
|
12946
|
+
* referenced. The schema declaration (`Field: z.…`) is not a `payload.` read,
|
|
12947
|
+
* so it never counts. Vocabulary-derived fields are out of scope. The generator emits the
|
|
12948
|
+
* fields no emitted request reads as optional schema fields, since a required field nothing
|
|
12949
|
+
* sends only burdens callers.
|
|
12950
|
+
*/
|
|
12951
|
+
function unreferencedDeclaredPayloadFields(contractCode, browserFlowCode, flowSteps) {
|
|
12952
|
+
const emittedCode = `${stripNonCodeText(contractCode)}\n${stripNonCodeText(browserFlowCode)}`;
|
|
12953
|
+
const declared = new Set();
|
|
12954
|
+
for (const step of flowSteps) {
|
|
12955
|
+
if (typeof step !== "string" && step.payloadField)
|
|
12956
|
+
declared.add(step.payloadField);
|
|
12957
|
+
}
|
|
12958
|
+
return [...declared].filter((name) => !new RegExp(`payload\\.${name}(?![\\w$])`).test(emittedCode));
|
|
12959
|
+
}
|
|
12960
|
+
/**
|
|
12961
|
+
* Declared payload fields the emitted requests never read. When the contract
|
|
12962
|
+
* replays HTTP requests only those count (the browser flow just changes
|
|
12963
|
+
* instruction text); a browser-only contract counts the browser flow too.
|
|
12964
|
+
* These are emitted optional so no caller has to supply a value that nothing
|
|
12965
|
+
* sends.
|
|
12966
|
+
*/
|
|
12967
|
+
function unreadDeclaredPayloadFields(contractCode, browserFlowCode, flowSteps) {
|
|
12968
|
+
return new Set(unreferencedDeclaredPayloadFields(contractCode, contractCode.includes("httpClient(") ? "" : browserFlowCode, flowSteps));
|
|
12969
|
+
}
|
|
12262
12970
|
/**
|
|
12263
12971
|
* Escape a literal string segment so it is safe INSIDE a JS backtick template
|
|
12264
12972
|
* literal — backslashes, backticks, and `${` interpolation starts must all be
|
|
@@ -12278,13 +12986,17 @@ function escapeForTemplateLiteral(segment) {
|
|
|
12278
12986
|
* own choice so the browser-flow and HTTP-body emitters never disagree on
|
|
12279
12987
|
* which quoted span is the persona value.
|
|
12280
12988
|
*/
|
|
12281
|
-
function buildStepInstructionExpr(instruction, field) {
|
|
12989
|
+
function buildStepInstructionExpr(instruction, field, capturedFallback = false) {
|
|
12282
12990
|
if (field === null)
|
|
12283
12991
|
return JSON.stringify(instruction);
|
|
12284
12992
|
const site = locateSpliceSite(instruction);
|
|
12285
12993
|
if (site === null)
|
|
12286
12994
|
return JSON.stringify(instruction);
|
|
12287
|
-
|
|
12995
|
+
const captured = pickedQuotedValue(instruction);
|
|
12996
|
+
const read = capturedFallback && captured !== null
|
|
12997
|
+
? `payload.${field} ?? ${JSON.stringify(captured)}`
|
|
12998
|
+
: `payload.${field}`;
|
|
12999
|
+
return `\`${escapeForTemplateLiteral(site.before)}\${${read}}${escapeForTemplateLiteral(site.after)}\``;
|
|
12288
13000
|
}
|
|
12289
13001
|
/**
|
|
12290
13002
|
* Build the emitted instruction expression for a step whose value is the
|
|
@@ -12351,6 +13063,22 @@ function jsonSchemaTypeOf(value) {
|
|
|
12351
13063
|
return "object";
|
|
12352
13064
|
return "string";
|
|
12353
13065
|
}
|
|
13066
|
+
/**
|
|
13067
|
+
* Whether the flow's last step declares itself non-submit in its own wording
|
|
13068
|
+
* (and carries no explicit `submitStep` flag). The submission-flow forcing must
|
|
13069
|
+
* skip it: an explicit flag overrides declared intent at runtime, so forcing it
|
|
13070
|
+
* onto a trailing guardrail step would route that step through the submit judge.
|
|
13071
|
+
*/
|
|
13072
|
+
function lastFlowStepDeclaresNonSubmit(flowSteps) {
|
|
13073
|
+
const last = flowSteps.at(-1);
|
|
13074
|
+
if (last === undefined)
|
|
13075
|
+
return false;
|
|
13076
|
+
const isObj = typeof last !== "string";
|
|
13077
|
+
return (0, flow_runner_1.isStepDeclaredNonSubmit)({
|
|
13078
|
+
step: isObj ? last.step : last,
|
|
13079
|
+
submitStep: isObj ? last.submitStep === true : false,
|
|
13080
|
+
});
|
|
13081
|
+
}
|
|
12354
13082
|
/**
|
|
12355
13083
|
* Emits a config-only plugin manifest (`<siteId>.plugin.json`) from the recon
|
|
12356
13084
|
* flow, as an alternative to the `.ts` trio for browser-only sites. Reuses the
|
|
@@ -12370,9 +13098,10 @@ function jsonSchemaTypeOf(value) {
|
|
|
12370
13098
|
* browser `flow` is the only execution path, and the field is omitted.
|
|
12371
13099
|
*/
|
|
12372
13100
|
function emitConfigManifest(opts) {
|
|
12373
|
-
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields, httpModulePath, isSubmissionFlow = false, env = process.env, } = opts;
|
|
13101
|
+
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields, httpModulePath, isSubmissionFlow = false, hasMultipartStep = false, browserFlowOnly = false, env = process.env, } = opts;
|
|
13102
|
+
const canonicalise = isSubmissionFlow && browserFlowOnly;
|
|
12374
13103
|
const payloadFieldNames = new Set();
|
|
12375
|
-
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env);
|
|
13104
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env, canonicalise);
|
|
12376
13105
|
const steps = flowSteps.map((step) => {
|
|
12377
13106
|
const isObj = typeof step !== "string";
|
|
12378
13107
|
const instruction = isObj ? step.step : step;
|
|
@@ -12399,7 +13128,7 @@ function emitConfigManifest(opts) {
|
|
|
12399
13128
|
...(emailStep ? { emailStep, emailStepConfig: emailStepConfig ?? {} } : {}),
|
|
12400
13129
|
};
|
|
12401
13130
|
}
|
|
12402
|
-
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
13131
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues, canonicalise);
|
|
12403
13132
|
if (field !== null)
|
|
12404
13133
|
payloadFieldNames.add(field);
|
|
12405
13134
|
const rewritten = buildManifestInstruction(instruction, field);
|
|
@@ -12424,7 +13153,7 @@ function emitConfigManifest(opts) {
|
|
|
12424
13153
|
// load of this manifest also gates on runHealingFlow's submitStep verification.
|
|
12425
13154
|
const lastStepIndex = steps.length - 1;
|
|
12426
13155
|
const lastStep = steps[lastStepIndex];
|
|
12427
|
-
if (isSubmissionFlow && lastStep !== undefined) {
|
|
13156
|
+
if (isSubmissionFlow && lastStep !== undefined && !lastFlowStepDeclaresNonSubmit(flowSteps)) {
|
|
12428
13157
|
steps[lastStepIndex] =
|
|
12429
13158
|
typeof lastStep === "string"
|
|
12430
13159
|
? { step: lastStep, optional: false, upload: false, submitStep: true }
|
|
@@ -12449,9 +13178,24 @@ function emitConfigManifest(opts) {
|
|
|
12449
13178
|
requestProperties[name] = { type: jsonSchemaTypeOf(value) };
|
|
12450
13179
|
}
|
|
12451
13180
|
}
|
|
13181
|
+
const usesApplicantContactSchema = contractUsesApplicantContactSchema({
|
|
13182
|
+
inputBody,
|
|
13183
|
+
isSubmissionFlow,
|
|
13184
|
+
browserFlowOnly,
|
|
13185
|
+
payloadFieldNames,
|
|
13186
|
+
});
|
|
13187
|
+
// Mirrors the .ts contract's JobTrackingSchema composition: a request schema
|
|
13188
|
+
// without it would drop the TrackingUrl the engine reads after submission.
|
|
13189
|
+
if (usesApplicantContactSchema)
|
|
13190
|
+
requestProperties.TrackingUrl ??= { type: "string" };
|
|
12452
13191
|
const sortedRequestProperties = Object.fromEntries(Object.keys(requestProperties)
|
|
12453
13192
|
.sort()
|
|
12454
13193
|
.map((name) => [name, requestProperties[name]]));
|
|
13194
|
+
// Same decision emitBrowserFlowTs makes: only a flow that uploads AND whose
|
|
13195
|
+
// contract carries Resume fields is multipart, so buildUploadFixture agrees.
|
|
13196
|
+
const hasUploadStep = flowSteps.some((s) => typeof s !== "string" && s.upload === true);
|
|
13197
|
+
const contractCarriesResume = hasMultipartStep || usesApplicantContactSchema;
|
|
13198
|
+
const multipart = hasUploadStep && contractCarriesResume;
|
|
12455
13199
|
const manifest = {
|
|
12456
13200
|
apiVersion: plugin_manifest_envelope_1.CONFIG_PLUGIN_API_VERSION,
|
|
12457
13201
|
kind: plugin_manifest_envelope_1.CONFIG_PLUGIN_KIND,
|
|
@@ -12459,6 +13203,7 @@ function emitConfigManifest(opts) {
|
|
|
12459
13203
|
spec: {
|
|
12460
13204
|
defaultBaseUrl: baseUrl,
|
|
12461
13205
|
...(httpModulePath ? { httpModule: httpModulePath } : {}),
|
|
13206
|
+
...(multipart ? { multipart: true } : {}),
|
|
12462
13207
|
request: { type: "object", properties: sortedRequestProperties },
|
|
12463
13208
|
response: {
|
|
12464
13209
|
type: "object",
|
|
@@ -12493,7 +13238,7 @@ function emitConfigManifest(opts) {
|
|
|
12493
13238
|
* facet-splice pass can correlate against the exact same declared field set
|
|
12494
13239
|
* instead of re-deriving a second, potentially-drifting one.
|
|
12495
13240
|
*/
|
|
12496
|
-
function computeFlowPayloadFieldNames(flowSteps, vocabulary, env) {
|
|
13241
|
+
function computeFlowPayloadFieldNames(flowSteps, vocabulary, env, canonicalise = false) {
|
|
12497
13242
|
const payloadFieldNames = new Set();
|
|
12498
13243
|
// Widest wins: a field is only optional if EVERY step that registers it
|
|
12499
13244
|
// does so as optional.
|
|
@@ -12507,7 +13252,7 @@ function computeFlowPayloadFieldNames(flowSteps, vocabulary, env) {
|
|
|
12507
13252
|
optionalPayloadFieldNames.delete(field);
|
|
12508
13253
|
}
|
|
12509
13254
|
};
|
|
12510
|
-
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env);
|
|
13255
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env, canonicalise);
|
|
12511
13256
|
for (const step of flowSteps) {
|
|
12512
13257
|
const isObj = typeof step !== "string";
|
|
12513
13258
|
const instruction = isObj ? step.step : step;
|
|
@@ -12534,7 +13279,7 @@ function computeFlowPayloadFieldNames(flowSteps, vocabulary, env) {
|
|
|
12534
13279
|
continue;
|
|
12535
13280
|
if (instruction.includes(RECON_PASSWORD_TOKEN))
|
|
12536
13281
|
continue;
|
|
12537
|
-
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
13282
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues, canonicalise);
|
|
12538
13283
|
const composite = field === null && !(isObj && step.payloadFieldNone)
|
|
12539
13284
|
? resolveCompositePersonaFields(instruction, knownFieldValues)
|
|
12540
13285
|
: null;
|
|
@@ -12557,10 +13302,22 @@ function computeFlowPayloadFieldNames(flowSteps, vocabulary, env) {
|
|
|
12557
13302
|
return { payloadFieldNames, optionalPayloadFieldNames };
|
|
12558
13303
|
}
|
|
12559
13304
|
function emitBrowserFlowTs(opts) {
|
|
12560
|
-
const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, vocabulary, frameSelector, env = process.env, } = opts;
|
|
12561
|
-
const
|
|
13305
|
+
const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, browserFlowOnly = false, inputBody, vocabulary, frameSelector, capturedFallbackFields = new Set(), env = process.env, } = opts;
|
|
13306
|
+
const canonicalise = isSubmissionFlow && browserFlowOnly;
|
|
13307
|
+
const { payloadFieldNames, optionalPayloadFieldNames } = computeFlowPayloadFieldNames(flowSteps, vocabulary, env, canonicalise);
|
|
13308
|
+
for (const field of capturedFallbackFields) {
|
|
13309
|
+
if (payloadFieldNames.has(field))
|
|
13310
|
+
optionalPayloadFieldNames.add(field);
|
|
13311
|
+
}
|
|
12562
13312
|
const hasUploadStep = flowSteps.some((s) => typeof s !== "string" && s.upload === true);
|
|
12563
|
-
const
|
|
13313
|
+
const contractCarriesResume = hasMultipartStep ||
|
|
13314
|
+
contractUsesApplicantContactSchema({
|
|
13315
|
+
inputBody,
|
|
13316
|
+
isSubmissionFlow,
|
|
13317
|
+
browserFlowOnly,
|
|
13318
|
+
payloadFieldNames,
|
|
13319
|
+
});
|
|
13320
|
+
const knownFieldValues = buildKnownFieldValues(flowSteps, vocabulary ?? vocabulary_1.EMPTY_VOCABULARY, env, canonicalise);
|
|
12564
13321
|
let usesThrowawayPassword = false;
|
|
12565
13322
|
let usesEmailStep = false;
|
|
12566
13323
|
const stepLiterals = flowSteps.map((step) => {
|
|
@@ -12602,14 +13359,14 @@ function emitBrowserFlowTs(opts) {
|
|
|
12602
13359
|
const submitStep = isObj ? step.submitStep === true : false;
|
|
12603
13360
|
return ` { instruction: ${instructionExpr}, optional: ${optional}, upload: ${upload}, submitStep: ${submitStep} },`;
|
|
12604
13361
|
}
|
|
12605
|
-
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues);
|
|
13362
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary, knownFieldValues, canonicalise);
|
|
12606
13363
|
const composite = field === null && !(isObj && step.payloadFieldNone)
|
|
12607
13364
|
? resolveCompositePersonaFields(instruction, knownFieldValues)
|
|
12608
13365
|
: null;
|
|
12609
13366
|
const optional = isObj ? step.optional === true : false;
|
|
12610
13367
|
const instructionExpr = composite !== null
|
|
12611
13368
|
? buildCompositeStepInstructionExpr(instruction, composite.fieldA, composite.fieldB)
|
|
12612
|
-
: buildStepInstructionExpr(instruction, field);
|
|
13369
|
+
: buildStepInstructionExpr(instruction, field, field !== null && capturedFallbackFields.has(field));
|
|
12613
13370
|
const upload = isObj ? step.upload === true : false;
|
|
12614
13371
|
const submitStep = isObj ? step.submitStep === true : false;
|
|
12615
13372
|
return ` { instruction: ${instructionExpr}, optional: ${optional}, upload: ${upload}, submitStep: ${submitStep} },`;
|
|
@@ -12618,7 +13375,9 @@ function emitBrowserFlowTs(opts) {
|
|
|
12618
13375
|
// submitStep:true even when the source recon-flow.json step never declared
|
|
12619
13376
|
// it, so the engine's pre-submit probe/StepVerificationError actually gates it.
|
|
12620
13377
|
const lastStepLiteral = stepLiterals.at(-1);
|
|
12621
|
-
if (isSubmissionFlow &&
|
|
13378
|
+
if (isSubmissionFlow &&
|
|
13379
|
+
lastStepLiteral !== undefined &&
|
|
13380
|
+
!lastFlowStepDeclaresNonSubmit(flowSteps)) {
|
|
12622
13381
|
stepLiterals[stepLiterals.length - 1] = lastStepLiteral.replace(/submitStep: (true|false)(?= \},)/, "submitStep: true");
|
|
12623
13382
|
}
|
|
12624
13383
|
const flowStepsBlock = stepLiterals.length > 0
|
|
@@ -12633,7 +13392,7 @@ function emitBrowserFlowTs(opts) {
|
|
|
12633
13392
|
// multipart detection is dropped), those payload fields don't exist yet — emit
|
|
12634
13393
|
// a null + TODO so the generated module still typechecks; the operator adds
|
|
12635
13394
|
// the multipart contract fields and wires the fixture during hand-finish.
|
|
12636
|
-
const uploadFixtureExpr = hasUploadStep &&
|
|
13395
|
+
const uploadFixtureExpr = hasUploadStep && contractCarriesResume
|
|
12637
13396
|
? `{
|
|
12638
13397
|
buffer: Buffer.from(payload.Resume),
|
|
12639
13398
|
name: payload.ResumeFilename ?? "resume.pdf",
|
|
@@ -12689,8 +13448,8 @@ export async function run${pascal}BrowserFlow(
|
|
|
12689
13448
|
): Promise<${pascal}Response> {
|
|
12690
13449
|
const page = await stagehand.context.awaitActivePage();
|
|
12691
13450
|
|
|
12692
|
-
await page.goto(${isSubmissionFlow ? "entryUrl" : "baseUrl"}, { waitUntil: "
|
|
12693
|
-
//
|
|
13451
|
+
await page.goto(${isSubmissionFlow ? "entryUrl" : "baseUrl"}, { waitUntil: "domcontentloaded" });
|
|
13452
|
+
// domcontentloaded fires before a bot-managed/CDN-fronted SPA hydrates; wait for
|
|
12694
13453
|
// the real DOM so the first steps don't probe an empty shell page and skip.
|
|
12695
13454
|
await waitForSpaReady(page, logger);
|
|
12696
13455
|
${usesThrowawayPassword ? "\n // Minted once per run — the flow needs a credential to authenticate, but\n // there is no caller-supplied Password field on the payload to splice.\n const throwawayPassword = generateThrowawayPassword();\n" : ""}${usesEmailStep ? "\n // Resolved once per run from the caller-supplied Email — the emailStep hook\n // polls this inbox rather than a freshly allocated one, since the applicant\n // (not this run) owns the address the ATS actually emails.\n const allocatedInbox = testmailInboxFromAddress(payload.Email);\n" : ""}
|
|
@@ -12747,7 +13506,7 @@ ${flowStepsBlock}
|
|
|
12747
13506
|
}
|
|
12748
13507
|
`}`;
|
|
12749
13508
|
assertNoLeakedPersonaConstant(code, knownFieldValues);
|
|
12750
|
-
return { code, payloadFieldNames, optionalPayloadFieldNames };
|
|
13509
|
+
return { code, payloadFieldNames, optionalPayloadFieldNames, usesThrowawayPassword };
|
|
12751
13510
|
}
|
|
12752
13511
|
/** Generates the site's index.ts barrel — exported so the out-of-tree e2e
|
|
12753
13512
|
* test can drive the emitter directly without spawning the CLI. */
|
|
@@ -13127,6 +13886,14 @@ async function main() {
|
|
|
13127
13886
|
};
|
|
13128
13887
|
}
|
|
13129
13888
|
})();
|
|
13889
|
+
const browserFlowOnlyOptIn = (() => {
|
|
13890
|
+
try {
|
|
13891
|
+
return parseBrowserFlowOnlyOptIn((0, node_fs_1.readFileSync)(flowFile, "utf8"));
|
|
13892
|
+
}
|
|
13893
|
+
catch {
|
|
13894
|
+
return false;
|
|
13895
|
+
}
|
|
13896
|
+
})();
|
|
13130
13897
|
// Flow-declared signals that isolate the submission POSTs from same-origin
|
|
13131
13898
|
// page chrome. Threaded into action-sequence extraction and header derivation
|
|
13132
13899
|
// so both draw from the real submission, not incidental widget/chatbot POSTs.
|
|
@@ -13163,7 +13930,9 @@ async function main() {
|
|
|
13163
13930
|
* already written its output and there is nothing left for main() to do.
|
|
13164
13931
|
*/
|
|
13165
13932
|
function generateFromCaptures(activeCaptures) {
|
|
13166
|
-
const baseUrl =
|
|
13933
|
+
const baseUrl = activeCaptures.length === 0
|
|
13934
|
+
? "https://example.com"
|
|
13935
|
+
: deriveBaseUrl(activeCaptures, ownBackendHostnames, submitPatterns);
|
|
13167
13936
|
// Gate on the flow's declared own-backend hosts (or the registrable-domain
|
|
13168
13937
|
// fallback of baseUrl) via the same predicate recon-http.ts applies at
|
|
13169
13938
|
// write time, so a stale aux/ directory from before that filter existed
|
|
@@ -13309,7 +14078,7 @@ async function main() {
|
|
|
13309
14078
|
logger.warn(`submission selection: declared submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) disagrees with the unfiltered heuristic action sequence (${unfilteredHeuristicActionCaptures.length} capture(s)); using the declared pattern because requireSubmitEndpointMatch is set`);
|
|
13310
14079
|
}
|
|
13311
14080
|
else if (patternUndercounts) {
|
|
13312
|
-
logger.info(`submission selection:
|
|
14081
|
+
logger.info(`submission selection: using declared submitEndpointPattern/submitBodyPattern (${patternedHeuristicActionCaptures.length} capture(s)) although the unfiltered heuristic action sequence finds ${unfilteredHeuristicActionCaptures.length} capture(s); the unfiltered set would make the generator attempt an HTTP replay that aborts on frozen-varying fields`);
|
|
13313
14082
|
}
|
|
13314
14083
|
const heuristicActionCaptures = patternedHeuristicActionCaptures;
|
|
13315
14084
|
const manifestActionCaptures = resolveManifestActionSequence(runRoot, activeCaptures);
|
|
@@ -13400,7 +14169,12 @@ async function main() {
|
|
|
13400
14169
|
? indexStateValues(activeCaptures, shieldedUuids, actionCaptureIndices, dependentDrillDownChainValues)
|
|
13401
14170
|
: new Map();
|
|
13402
14171
|
const actionSteps = actionCaptures.length > 1 ? compileActionSteps(actionCaptures, stateIndex) : [];
|
|
13403
|
-
|
|
14172
|
+
// A multi-step flow whose declared submit pattern selects no captures has no
|
|
14173
|
+
// replayable HTTP sequence, yet is still not a search: treat it as a
|
|
14174
|
+
// browser-flow-only submission flow rather than the single-endpoint `{query}` branch.
|
|
14175
|
+
const noReplayableActionFlow = !gql && actionSteps.length === 0 && flowSteps.length > 1;
|
|
14176
|
+
const isSubmissionFlow = noReplayableActionFlow ||
|
|
14177
|
+
(actionSteps.length > 1 && (!gql || graphqlActionSequenceHasMutation));
|
|
13404
14178
|
// Diagnostic for the FAILURE-3 shape (a flowless recon capture): a "submission flow"
|
|
13405
14179
|
// whose every action capture is landing-phase is almost certainly page-chrome
|
|
13406
14180
|
// bootstrap (e.g. page-chrome `POST /widgets`) misread as an apply flow, not a walked
|
|
@@ -13502,7 +14276,8 @@ async function main() {
|
|
|
13502
14276
|
// downgrade to the single-endpoint `{query}` branch either, since that's a
|
|
13503
14277
|
// fabrication of its own kind for a flow that isn't a single-action
|
|
13504
14278
|
// query/search to begin with.
|
|
13505
|
-
const browserFlowOnly = isSubmissionFlow &&
|
|
14279
|
+
const browserFlowOnly = isSubmissionFlow &&
|
|
14280
|
+
(browserFlowOnlyOptIn || noReplayableActionFlow || actionSteps.some((s) => s.isCrossDomain));
|
|
13506
14281
|
const multiStepBody = browserFlowOnly
|
|
13507
14282
|
? undefined
|
|
13508
14283
|
: isSubmissionFlow
|
|
@@ -13570,7 +14345,7 @@ async function main() {
|
|
|
13570
14345
|
if (foldReturnSpec !== null && effectiveFoldPlanCount > 0 && !declaredJoinFieldsApplied) {
|
|
13571
14346
|
logger.warn(`flow declares foldReturn (endpointPattern: ${foldReturnSpec.endpointPattern}, resultsPath: ${foldReturnSpec.resultsPath}, joinFields: ${foldReturnSpec.joinFields.join(", ")}) but the declared spec resolved no fold plan — only structurally-detected fold plans (with their own guessed join fields) were emitted, so the declared joinFields were not applied; check that the endpointPattern names a capture in the action sequence and resultsPath resolves on the primary response`);
|
|
13572
14347
|
}
|
|
13573
|
-
logger.info(`generating plugin for ${siteId} (${gql ? "GraphQL" : browserFlowOnly ? `submission flow, ${actionSteps.length} steps, browser-flow-only (cross-domain hop detected)` : isSubmissionFlow ? `submission flow, ${actionSteps.length} steps` : "single-endpoint REST"}, baseUrl: ${baseUrl})`);
|
|
14348
|
+
logger.info(`generating plugin for ${siteId} (${gql ? "GraphQL" : browserFlowOnly ? `submission flow, ${actionSteps.length} steps, browser-flow-only (${browserFlowOnlyOptIn ? "forced by flow file" : noReplayableActionFlow ? "no replayable action sequence" : "cross-domain hop detected"})` : isSubmissionFlow ? `submission flow, ${actionSteps.length} steps` : "single-endpoint REST"}, baseUrl: ${baseUrl})`);
|
|
13574
14349
|
if (emit === "config") {
|
|
13575
14350
|
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
13576
14351
|
(0, node_fs_1.writeFileSync)(manifestPath, emitConfigManifest({
|
|
@@ -13582,6 +14357,8 @@ async function main() {
|
|
|
13582
14357
|
inputBody,
|
|
13583
14358
|
recoveredFields: [...discoveredFormFields, ...discoveredOptionFields],
|
|
13584
14359
|
isSubmissionFlow,
|
|
14360
|
+
hasMultipartStep,
|
|
14361
|
+
browserFlowOnly,
|
|
13585
14362
|
// A submission flow is the case where the `.ts` emit carries an
|
|
13586
14363
|
// executeHttp hot path; point the manifest at where the operator drops
|
|
13587
14364
|
// the compiled module rather than silently dropping the direct path.
|
|
@@ -13604,6 +14381,8 @@ async function main() {
|
|
|
13604
14381
|
flowSteps,
|
|
13605
14382
|
isSubmissionFlow,
|
|
13606
14383
|
hasMultipartStep,
|
|
14384
|
+
browserFlowOnly,
|
|
14385
|
+
inputBody,
|
|
13607
14386
|
vocabulary,
|
|
13608
14387
|
frameSelector,
|
|
13609
14388
|
});
|
|
@@ -13642,6 +14421,7 @@ async function main() {
|
|
|
13642
14421
|
auxFiles,
|
|
13643
14422
|
multiStepBody,
|
|
13644
14423
|
omitExecuteHttp: browserFlowOnly,
|
|
14424
|
+
createsAccount: browserFlow.usesThrowawayPassword,
|
|
13645
14425
|
isSubmissionFlow,
|
|
13646
14426
|
inputBody,
|
|
13647
14427
|
hasMultipartStep,
|
|
@@ -13661,11 +14441,43 @@ async function main() {
|
|
|
13661
14441
|
unpopulatedDeclaredVariables: primaryGraphQLOperation?.unpopulatedDeclaredVariables ?? [],
|
|
13662
14442
|
fallbackGateSpec,
|
|
13663
14443
|
};
|
|
13664
|
-
const
|
|
14444
|
+
const firstContractCode = emitContractTs(contractOpts);
|
|
14445
|
+
const unread = unreadDeclaredPayloadFields(firstContractCode, browserFlow.code, flowSteps);
|
|
14446
|
+
// A field no emitted request reads must not be required of callers. The
|
|
14447
|
+
// browser fallback keeps splicing it, from the captured value when the
|
|
14448
|
+
// caller omits it, so the field still takes effect when supplied.
|
|
14449
|
+
const settled = unread.size === 0
|
|
14450
|
+
? { browserFlow, contractOpts, contractCode: firstContractCode }
|
|
14451
|
+
: (() => {
|
|
14452
|
+
const fallbackBrowserFlow = emitBrowserFlowTs({
|
|
14453
|
+
siteId,
|
|
14454
|
+
pascal,
|
|
14455
|
+
baseUrl,
|
|
14456
|
+
flowSteps,
|
|
14457
|
+
isSubmissionFlow,
|
|
14458
|
+
hasMultipartStep,
|
|
14459
|
+
browserFlowOnly,
|
|
14460
|
+
vocabulary,
|
|
14461
|
+
frameSelector,
|
|
14462
|
+
capturedFallbackFields: unread,
|
|
14463
|
+
});
|
|
14464
|
+
const optionalized = {
|
|
14465
|
+
...contractOpts,
|
|
14466
|
+
optionalPayloadFieldNames: new Set([
|
|
14467
|
+
...fallbackBrowserFlow.optionalPayloadFieldNames,
|
|
14468
|
+
...unread,
|
|
14469
|
+
]),
|
|
14470
|
+
};
|
|
14471
|
+
return {
|
|
14472
|
+
browserFlow: fallbackBrowserFlow,
|
|
14473
|
+
contractOpts: optionalized,
|
|
14474
|
+
contractCode: emitContractTs(optionalized),
|
|
14475
|
+
};
|
|
14476
|
+
})();
|
|
13665
14477
|
return {
|
|
13666
|
-
contractCode,
|
|
13667
|
-
contractOpts,
|
|
13668
|
-
browserFlow,
|
|
14478
|
+
contractCode: settled.contractCode,
|
|
14479
|
+
contractOpts: settled.contractOpts,
|
|
14480
|
+
browserFlow: settled.browserFlow,
|
|
13669
14481
|
auxFiles,
|
|
13670
14482
|
resolvedPool: actionCaptures,
|
|
13671
14483
|
isSubmissionFlow,
|
|
@@ -13677,6 +14489,10 @@ async function main() {
|
|
|
13677
14489
|
if (result === null)
|
|
13678
14490
|
return; // emit === "config": already written above.
|
|
13679
14491
|
const final = healUnreferencedUrlFieldsOnce(captures, result, generateFromCaptures);
|
|
14492
|
+
const unreferencedEverywhere = new Set(unreferencedDeclaredPayloadFields(final.contractCode, final.browserFlow.code, flowSteps));
|
|
14493
|
+
for (const field of unreadDeclaredPayloadFields(final.contractCode, final.browserFlow.code, flowSteps)) {
|
|
14494
|
+
logger.warn(`declared payloadField ${field} is read by no emitted request; emitted as optional schema field${unreferencedEverywhere.has(field) ? "" : " (only the browser flow splices it)"}`);
|
|
14495
|
+
}
|
|
13680
14496
|
(0, node_fs_1.writeFileSync)(`${outDir}/contract.ts`, final.contractCode);
|
|
13681
14497
|
logger.info(`wrote ${outDir}/contract.ts`);
|
|
13682
14498
|
// Same opts fed to emitContractTs above, so this can never drift from what
|