@enricai/barnacle 1.12.54 → 1.12.55
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/schemas/common.d.ts +28 -0
- package/dist/api/schemas/common.d.ts.map +1 -1
- package/dist/api/schemas/common.js +51 -0
- package/dist/api/schemas/common.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 +56 -3
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/json-schema-to-zod.d.ts +6 -3
- package/dist/plugins/json-schema-to-zod.d.ts.map +1 -1
- package/dist/plugins/json-schema-to-zod.js +18 -6
- package/dist/plugins/json-schema-to-zod.js.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +7 -1
- package/dist/plugins/loader.js.map +1 -1
- package/dist/recon/capture-filters.d.ts +2 -55
- package/dist/recon/capture-filters.d.ts.map +1 -1
- package/dist/recon/capture-filters.js +152 -4
- package/dist/recon/capture-filters.js.map +1 -1
- package/dist/recon/load-value-constraints.d.ts +22 -0
- package/dist/recon/load-value-constraints.d.ts.map +1 -0
- package/dist/recon/load-value-constraints.js +61 -0
- package/dist/recon/load-value-constraints.js.map +1 -0
- package/dist/recon/value-constraints.d.ts +46 -0
- package/dist/recon/value-constraints.d.ts.map +1 -0
- package/dist/recon/value-constraints.js +31 -0
- package/dist/recon/value-constraints.js.map +1 -0
- package/dist/scraper/errors.d.ts +12 -0
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +21 -1
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +87 -23
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +417 -71
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts +11 -0
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +96 -2
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/phantom-click.d.ts +2 -2
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +6 -5
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +11 -6
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +38 -7
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +45 -8
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +145 -16
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +65 -4
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +387 -49
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/site-plugin.d.ts +9 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -66,6 +66,7 @@ exports.getScanPrimaryCandidateGroupsCallCountForTest = getScanPrimaryCandidateG
|
|
|
66
66
|
exports.resetScanPrimaryCandidateGroupsCallCountForTest = resetScanPrimaryCandidateGroupsCallCountForTest;
|
|
67
67
|
exports.detectDrillDownFoldPlan = detectDrillDownFoldPlan;
|
|
68
68
|
exports.parseFoldReturnSpec = parseFoldReturnSpec;
|
|
69
|
+
exports.parseFallbackGateSpec = parseFallbackGateSpec;
|
|
69
70
|
exports.collectRequestValuesIncludingHeaders = collectRequestValuesIncludingHeaders;
|
|
70
71
|
exports.getFoldPlanResolutionCallCountForTests = getFoldPlanResolutionCallCountForTests;
|
|
71
72
|
exports.resetFoldPlanResolutionCallCountForTests = resetFoldPlanResolutionCallCountForTests;
|
|
@@ -89,7 +90,9 @@ const plugin_api_version_1 = require("../plugins/plugin-api-version");
|
|
|
89
90
|
const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
|
|
90
91
|
const capture_filters_1 = require("../recon/capture-filters");
|
|
91
92
|
const load_form_schema_1 = require("../recon/load-form-schema");
|
|
93
|
+
const load_value_constraints_1 = require("../recon/load-value-constraints");
|
|
92
94
|
const load_vocabulary_1 = require("../recon/load-vocabulary");
|
|
95
|
+
const value_constraints_1 = require("../recon/value-constraints");
|
|
93
96
|
const vocabulary_1 = require("../recon/vocabulary");
|
|
94
97
|
const recon_shared_1 = require("./recon-shared");
|
|
95
98
|
const logger = (0, logging_1.getScriptLogger)("recon-generate");
|
|
@@ -1798,6 +1801,17 @@ function collapseRedundantPatches(actions) {
|
|
|
1798
1801
|
* counter, unpaired with an explicit page-size key) since that is the common
|
|
1799
1802
|
* REST shape, unlike GraphQL's paired variables convention. */
|
|
1800
1803
|
const PAGINATION_FIELD_NAME_PATTERN = /^(page|pagenum|pagenumber|pageindex|pageno|offset|skip|start|cursor)$/i;
|
|
1804
|
+
/** Response leaf-key shapes that name a discoverable ceiling on some other
|
|
1805
|
+
* numeric quantity (a capacity, a maximum, a limit) -- structural, like
|
|
1806
|
+
* {@link PAGINATION_FIELD_NAME_PATTERN}, not tied to any domain's field
|
|
1807
|
+
* vocabulary. Used to bound a request-side numeric field whose observed
|
|
1808
|
+
* values never exceed a same-run response's declared ceiling. */
|
|
1809
|
+
const CAPACITY_FIELD_NAME_PATTERN = /(max|capacity|limit)/i;
|
|
1810
|
+
/** Upper bound on how many distinct string values a request field may show
|
|
1811
|
+
* across a run's captures before it stops looking like a closed-set facet
|
|
1812
|
+
* vocabulary and starts looking like free text -- past this, emitting
|
|
1813
|
+
* z.enum(...) would lock callers out of values recon simply never sampled. */
|
|
1814
|
+
const VOCABULARY_ENUM_MAX_DISTINCT_VALUES = 8;
|
|
1801
1815
|
/** Request-field key names that name known client-generated scaffolding
|
|
1802
1816
|
* (a monotonic sequence counter, a correlation/trace id, an idempotency
|
|
1803
1817
|
* nonce) rather than genuine payload data. Gates {@link
|
|
@@ -3928,6 +3942,41 @@ function pathToFoldLoopLines(expr, path, itemVar, indent, varSuffix = "", depth
|
|
|
3928
3942
|
ancestorCloseLines,
|
|
3929
3943
|
};
|
|
3930
3944
|
}
|
|
3945
|
+
/**
|
|
3946
|
+
* Splices a fold's per-item body between `itemOpenLines`/`itemCloseLines`
|
|
3947
|
+
* (see {@link pathToFoldLoopLines}) — either as the original sequential
|
|
3948
|
+
* `for` loop, when the body never issues its own per-item drill fetch (so
|
|
3949
|
+
* existing byte-identical fold output is preserved for every fold that has
|
|
3950
|
+
* nothing to parallelize), or rewritten into a `Promise.allSettled`-based
|
|
3951
|
+
* parallel form when it does: `for (const item of X) {` becomes `await
|
|
3952
|
+
* Promise.allSettled((X).map(async (item) => { ... }));`. Every item's
|
|
3953
|
+
* fetch then fires concurrently instead of one at a time, and a rejected
|
|
3954
|
+
* item's promise can't abort a sibling's — `allSettled` never short-
|
|
3955
|
+
* circuits on a rejection, and the sibling's `Object.assign` onto its own
|
|
3956
|
+
* item already only runs once ITS OWN fetch resolves, so a failed item
|
|
3957
|
+
* simply keeps its original (unmerged) fields instead of taking every other
|
|
3958
|
+
* item's already-fetched data down with it.
|
|
3959
|
+
*/
|
|
3960
|
+
function emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch) {
|
|
3961
|
+
if (!hasItemScopedFetch) {
|
|
3962
|
+
return [...itemOpenLines, ...itemScopedLines, ...itemCloseLines];
|
|
3963
|
+
}
|
|
3964
|
+
const forLineIndex = itemOpenLines.length - 1;
|
|
3965
|
+
const forLine = itemOpenLines[forLineIndex];
|
|
3966
|
+
const forLineMatch = forLine.match(/^(\s*)for \(const \w+ of (.+)\) \{$/);
|
|
3967
|
+
if (!forLineMatch) {
|
|
3968
|
+
throw new Error(`emitItemLoopLines: unrecognized fold item-loop shape: ${forLine}`);
|
|
3969
|
+
}
|
|
3970
|
+
const [, indent, iterableExpr] = forLineMatch;
|
|
3971
|
+
return [
|
|
3972
|
+
...itemOpenLines.slice(0, forLineIndex),
|
|
3973
|
+
`${indent}await Promise.allSettled(`,
|
|
3974
|
+
`${indent} (${iterableExpr}).map(async (${itemVar}) => {`,
|
|
3975
|
+
...itemScopedLines,
|
|
3976
|
+
`${indent} })`,
|
|
3977
|
+
`${indent});`,
|
|
3978
|
+
];
|
|
3979
|
+
}
|
|
3931
3980
|
/** Suggests a JS-camelCase variable name for a state value path. Falls back
|
|
3932
3981
|
* up the path if the tail is numeric or not a valid JS identifier. */
|
|
3933
3982
|
function pathToVarName(path) {
|
|
@@ -4817,10 +4866,15 @@ function applyUrlParamPayloadSubstitutions(template, parsedBody, bindings) {
|
|
|
4817
4866
|
* even though the field genuinely IS one this step's own request sends as
|
|
4818
4867
|
* caller-supplied data.
|
|
4819
4868
|
*/
|
|
4820
|
-
function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies = [], outAdditionalKeys = new Map()
|
|
4869
|
+
function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies = [], outAdditionalKeys = new Map(),
|
|
4870
|
+
/** Every capture's response body from the same run, scanned only for
|
|
4871
|
+
* {@link CAPACITY_FIELD_NAME_PATTERN}-shaped numeric leaves — the
|
|
4872
|
+
* evidence source for a discovered "number" key's `capacityMax`. */
|
|
4873
|
+
responseBodiesForCapacitySignal = []) {
|
|
4821
4874
|
const merged = [];
|
|
4822
4875
|
const seenPairs = new Set();
|
|
4823
4876
|
const seenValueByKey = new Map();
|
|
4877
|
+
const distinctValuesByKey = new Map();
|
|
4824
4878
|
const allBodies = [inputBody, ...additionalBodies];
|
|
4825
4879
|
for (const body of allBodies) {
|
|
4826
4880
|
if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -4854,6 +4908,17 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
|
|
|
4854
4908
|
continue;
|
|
4855
4909
|
}
|
|
4856
4910
|
seenValueByKey.set(key, value);
|
|
4911
|
+
// Tracked independently of the (key, value) dedupe below — a facet's
|
|
4912
|
+
// vocabulary or a capacity ceiling needs every distinct value it took,
|
|
4913
|
+
// not just the deduped substitution list, and pagination cursors are
|
|
4914
|
+
// excluded from both (their values grow monotonically and are never a
|
|
4915
|
+
// closed set).
|
|
4916
|
+
if ((typeof value === "string" || typeof value === "number") &&
|
|
4917
|
+
!PAGINATION_FIELD_NAME_PATTERN.test(key)) {
|
|
4918
|
+
const distinct = distinctValuesByKey.get(key) ?? new Set();
|
|
4919
|
+
distinct.add(value);
|
|
4920
|
+
distinctValuesByKey.set(key, distinct);
|
|
4921
|
+
}
|
|
4857
4922
|
const pairKey = `${key} ${typeof value} ${value}`;
|
|
4858
4923
|
if (seenPairs.has(pairKey))
|
|
4859
4924
|
continue;
|
|
@@ -4866,11 +4931,54 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
|
|
|
4866
4931
|
// isReservedByApplicantContactSchema — this function has no visibility
|
|
4867
4932
|
// into that flag, so it must not special-case inputBody's own keys.
|
|
4868
4933
|
if (typeof value === "string")
|
|
4869
|
-
outAdditionalKeys.set(key, "string");
|
|
4934
|
+
outAdditionalKeys.set(key, { kind: "string" });
|
|
4870
4935
|
else if (typeof value === "number")
|
|
4871
|
-
outAdditionalKeys.set(key, "number");
|
|
4936
|
+
outAdditionalKeys.set(key, { kind: "number" });
|
|
4872
4937
|
else if (typeof value === "boolean")
|
|
4873
|
-
outAdditionalKeys.set(key, "boolean");
|
|
4938
|
+
outAdditionalKeys.set(key, { kind: "boolean" });
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
// Vocabulary-derived enum: a "string" key whose distinct observed values
|
|
4942
|
+
// form a small closed set is a facet field, not free text — emit the set
|
|
4943
|
+
// so the generated schema rejects a caller value recon never sampled.
|
|
4944
|
+
// Capacity-bounded number: a "number" key gets a ceiling only when some
|
|
4945
|
+
// same-run response carries a max/capacity/limit-shaped numeric leaf that
|
|
4946
|
+
// is >= every value this key was observed to carry; the tightest such
|
|
4947
|
+
// ceiling wins so the bound is never looser than the evidence supports.
|
|
4948
|
+
const capacityCandidatesByMinBound = () => {
|
|
4949
|
+
const candidates = [];
|
|
4950
|
+
for (const body of responseBodiesForCapacitySignal) {
|
|
4951
|
+
if (body === undefined || body === null || typeof body !== "object")
|
|
4952
|
+
continue;
|
|
4953
|
+
for (const { value, path } of walkAllPrimitiveLeaves(body)) {
|
|
4954
|
+
const leafName = path[path.length - 1] ?? "";
|
|
4955
|
+
if (typeof value !== "number")
|
|
4956
|
+
continue;
|
|
4957
|
+
if (!CAPACITY_FIELD_NAME_PATTERN.test(leafName))
|
|
4958
|
+
continue;
|
|
4959
|
+
candidates.push(value);
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
return candidates;
|
|
4963
|
+
};
|
|
4964
|
+
const capacitySignals = capacityCandidatesByMinBound();
|
|
4965
|
+
for (const [key, info] of outAdditionalKeys) {
|
|
4966
|
+
const distinct = distinctValuesByKey.get(key);
|
|
4967
|
+
if (!distinct)
|
|
4968
|
+
continue;
|
|
4969
|
+
if (info.kind === "string") {
|
|
4970
|
+
if (distinct.size >= 2 && distinct.size <= VOCABULARY_ENUM_MAX_DISTINCT_VALUES) {
|
|
4971
|
+
info.enumValues = [...distinct].map(String).sort();
|
|
4972
|
+
}
|
|
4973
|
+
continue;
|
|
4974
|
+
}
|
|
4975
|
+
if (info.kind === "number") {
|
|
4976
|
+
const maxObserved = Math.max(...[...distinct].map(Number));
|
|
4977
|
+
const tightestCeiling = capacitySignals
|
|
4978
|
+
.filter((v) => v >= maxObserved)
|
|
4979
|
+
.sort((a, b) => a - b)[0];
|
|
4980
|
+
if (tightestCeiling !== undefined)
|
|
4981
|
+
info.capacityMax = tightestCeiling;
|
|
4874
4982
|
}
|
|
4875
4983
|
}
|
|
4876
4984
|
let result = template;
|
|
@@ -5254,6 +5362,12 @@ pascalName = null) {
|
|
|
5254
5362
|
// skip non-JSON bodies (e.g. multipart raw bytes)
|
|
5255
5363
|
}
|
|
5256
5364
|
}
|
|
5365
|
+
// Every action's response body, gathered once for the whole flow so a
|
|
5366
|
+
// capacity/max ceiling discovered on ANY capture (not just the one that
|
|
5367
|
+
// carries the request field itself) is available to bound a request-side
|
|
5368
|
+
// numeric field — the ceiling and the field it bounds are frequently
|
|
5369
|
+
// declared on different calls (an availability check, then a booking).
|
|
5370
|
+
const allResponseBodies = actions.map((a) => a.capture.responseBody);
|
|
5257
5371
|
// Every value `applyPayloadKeyValueSubstitutions` will unconditionally
|
|
5258
5372
|
// payload-ify from the ENTRY payload's own top-level key/value pairs,
|
|
5259
5373
|
// gathered here so {@link interpolateStateValues} can give it payload
|
|
@@ -5457,7 +5571,7 @@ pascalName = null) {
|
|
|
5457
5571
|
? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
|
|
5458
5572
|
: rawBodyWithProducerBoundary;
|
|
5459
5573
|
const bodyAfterStateAndKv = rawBodyWithUrlParams
|
|
5460
|
-
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
|
|
5574
|
+
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies)
|
|
5461
5575
|
: "";
|
|
5462
5576
|
// Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
|
|
5463
5577
|
// rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
|
|
@@ -5662,6 +5776,15 @@ pascalName = null) {
|
|
|
5662
5776
|
// Every target's join/merge — plus any non-hoistable target's own
|
|
5663
5777
|
// chain fetch — goes here, spliced inside the item loop.
|
|
5664
5778
|
const itemScopedLines = [];
|
|
5779
|
+
// True once any target's own chain fetch ends up item-scoped (not
|
|
5780
|
+
// hoisted above the item loop) — only then does the item loop have
|
|
5781
|
+
// anything to parallelize; see emitItemLoopLines.
|
|
5782
|
+
let hasItemScopedFetch = false;
|
|
5783
|
+
// True once any item-scoped target's chain threads a response header
|
|
5784
|
+
// through createHttpClient's shared `bind` store — see
|
|
5785
|
+
// chainUsesHeaderThreading below for why that disqualifies
|
|
5786
|
+
// parallelizing the item loop.
|
|
5787
|
+
let hasItemScopedHeaderThreading = false;
|
|
5665
5788
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
5666
5789
|
// `firstItem` decides which captured literal `parameterize` rewrites
|
|
5667
5790
|
// — it must be the item at `primaryMatchedItemIndex`, the one THIS
|
|
@@ -5896,6 +6019,16 @@ pascalName = null) {
|
|
|
5896
6019
|
// `itemVar` is never declared.
|
|
5897
6020
|
const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
|
|
5898
6021
|
let referencesItemVar = ancestorVars.length === 0;
|
|
6022
|
+
// A chain step that produces a response HEADER threads it through
|
|
6023
|
+
// `createHttpClient`'s single shared `bind` store (see
|
|
6024
|
+
// HttpClientOptions.bind), not a local variable — that store is
|
|
6025
|
+
// mutated in call order and read by whichever call happens to run
|
|
6026
|
+
// next, regardless of which item minted it. Running items
|
|
6027
|
+
// concurrently would let one item's header overwrite another's
|
|
6028
|
+
// in-flight header before its own later chain hop reads it back, so
|
|
6029
|
+
// any target relying on header threading keeps the item loop
|
|
6030
|
+
// sequential rather than risk cross-item header contamination.
|
|
6031
|
+
let chainUsesHeaderThreading = false;
|
|
5899
6032
|
for (const chainIndex of target.chain) {
|
|
5900
6033
|
const chainStep = actions[chainIndex];
|
|
5901
6034
|
const chainRendered = rendered[chainIndex];
|
|
@@ -5916,8 +6049,10 @@ pascalName = null) {
|
|
|
5916
6049
|
chainLines.push(` ${joined}`);
|
|
5917
6050
|
chainLines.push(` schema: ${chainRendered.schemaExpr},`, ` })) as Record<string, unknown>;`);
|
|
5918
6051
|
for (const p of chainStep.produces) {
|
|
5919
|
-
if (p.kind === "header")
|
|
6052
|
+
if (p.kind === "header") {
|
|
6053
|
+
chainUsesHeaderThreading = true;
|
|
5920
6054
|
continue;
|
|
6055
|
+
}
|
|
5921
6056
|
if (chainDeclared.has(p.name))
|
|
5922
6057
|
continue;
|
|
5923
6058
|
if (!referencedNames.has(p.name))
|
|
@@ -5931,13 +6066,17 @@ pascalName = null) {
|
|
|
5931
6066
|
const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
|
|
5932
6067
|
if (referencesItemVar) {
|
|
5933
6068
|
itemScopedLines.push(...chainLines, ...matchLines);
|
|
6069
|
+
if (chainLines.length > 0)
|
|
6070
|
+
hasItemScopedFetch = true;
|
|
6071
|
+
if (chainUsesHeaderThreading)
|
|
6072
|
+
hasItemScopedHeaderThreading = true;
|
|
5934
6073
|
}
|
|
5935
6074
|
else {
|
|
5936
6075
|
hoistedChainLines.push(...chainLines);
|
|
5937
6076
|
itemScopedLines.push(...matchLines);
|
|
5938
6077
|
}
|
|
5939
6078
|
}
|
|
5940
|
-
lines.push(...hoistedChainLines, ...itemOpenLines,
|
|
6079
|
+
lines.push(...hoistedChainLines, ...emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch && !hasItemScopedHeaderThreading), ...ancestorCloseLines, "");
|
|
5941
6080
|
continue;
|
|
5942
6081
|
}
|
|
5943
6082
|
// Every other chain step (already fully emitted, inline, by the fold
|
|
@@ -7616,6 +7755,41 @@ function parseFoldReturnSpec(flowFileContents) {
|
|
|
7616
7755
|
return null;
|
|
7617
7756
|
}
|
|
7618
7757
|
}
|
|
7758
|
+
/**
|
|
7759
|
+
* Parses `browserFallbackGate`/`httpTimeoutMs` out of an object-form
|
|
7760
|
+
* recon-flow.json, mirroring {@link parseFoldReturnSpec}'s null-safe pattern:
|
|
7761
|
+
* a missing file, a legacy bare-array flow, or a malformed declaration all
|
|
7762
|
+
* resolve to an empty spec (today's behavior) rather than aborting
|
|
7763
|
+
* generation over a field that only tightens an opt-in guard.
|
|
7764
|
+
*/
|
|
7765
|
+
function parseFallbackGateSpec(flowFileContents) {
|
|
7766
|
+
try {
|
|
7767
|
+
const raw = JSON.parse(flowFileContents);
|
|
7768
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
7769
|
+
return {};
|
|
7770
|
+
const { browserFallbackGate, httpTimeoutMs } = raw;
|
|
7771
|
+
const resolvedGate = (() => {
|
|
7772
|
+
if (browserFallbackGate === false)
|
|
7773
|
+
return false;
|
|
7774
|
+
if (Array.isArray(browserFallbackGate) &&
|
|
7775
|
+
browserFallbackGate.length > 0 &&
|
|
7776
|
+
browserFallbackGate.every((n) => typeof n === "string" && n.length > 0)) {
|
|
7777
|
+
return browserFallbackGate;
|
|
7778
|
+
}
|
|
7779
|
+
return undefined;
|
|
7780
|
+
})();
|
|
7781
|
+
const resolvedTimeout = typeof httpTimeoutMs === "number" && Number.isFinite(httpTimeoutMs) && httpTimeoutMs > 0
|
|
7782
|
+
? httpTimeoutMs
|
|
7783
|
+
: undefined;
|
|
7784
|
+
return {
|
|
7785
|
+
...(resolvedGate !== undefined ? { browserFallbackGate: resolvedGate } : {}),
|
|
7786
|
+
...(resolvedTimeout !== undefined ? { httpTimeoutMs: resolvedTimeout } : {}),
|
|
7787
|
+
};
|
|
7788
|
+
}
|
|
7789
|
+
catch {
|
|
7790
|
+
return {};
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7619
7793
|
/** The JS `typeof` a `drillParamBindings` entry's `default` must match for
|
|
7620
7794
|
* its declared `type` — the same "assert the type, then check the default
|
|
7621
7795
|
* agrees" pattern the rest of {@link parseFoldReturnSpec} uses for its other
|
|
@@ -8724,11 +8898,17 @@ function buildNestedSpreadOverride(base, path, leafExpr) {
|
|
|
8724
8898
|
return `{ ...${base}, ${keyExpr}: ${nested} }`;
|
|
8725
8899
|
}
|
|
8726
8900
|
/**
|
|
8727
|
-
* Emits a bounded paging loop for a read-only
|
|
8728
|
-
*
|
|
8729
|
-
*
|
|
8730
|
-
*
|
|
8731
|
-
*
|
|
8901
|
+
* Emits a bounded paging loop for a read-only primary operation exposing
|
|
8902
|
+
* {@link PaginationSignal}: advances the skip/offset variable by the
|
|
8903
|
+
* observed page size on each call, stops once the response's own reported
|
|
8904
|
+
* total is reached or `MAX_PAGES` caps it, and merges pages by the detected
|
|
8905
|
+
* identity field rather than concatenating blindly.
|
|
8906
|
+
*
|
|
8907
|
+
* The primary operation may be GraphQL (each page issued via `getGql`) or a
|
|
8908
|
+
* plain REST endpoint (each page issued via `httpClient`) — `fetchCall`
|
|
8909
|
+
* selects which, and is the only call-shape-specific piece of the loop; the
|
|
8910
|
+
* skip/PAGE_SIZE/MAX_PAGES bookkeeping, de-dup, and halt guards below are
|
|
8911
|
+
* shared verbatim between both.
|
|
8732
8912
|
*
|
|
8733
8913
|
* `foldMergeLines`, when non-empty, threads a resolved single-primary fold
|
|
8734
8914
|
* plan (see `emitContractTs`'s `singlePrimaryFoldPlans`) additively into the
|
|
@@ -8738,9 +8918,14 @@ function buildNestedSpreadOverride(base, path, leafExpr) {
|
|
|
8738
8918
|
* mutates its item in place (`Object.assign`), which is visible through
|
|
8739
8919
|
* `itemsById`'s own stored references — no re-`set` needed.
|
|
8740
8920
|
*/
|
|
8741
|
-
function
|
|
8742
|
-
const { pascal,
|
|
8921
|
+
function buildPaginatedFetchLoopExecuteHttpBody(opts) {
|
|
8922
|
+
const { pascal, gqlVariablesExpr, signal, foldMergeLines, fetchCall, valueConstraints } = opts;
|
|
8743
8923
|
const { totalPath, arrayPath, containerPath, countKey, skipKey, pageSize, identityField } = signal;
|
|
8924
|
+
const declaredMax = valueConstraints?.[countKey]?.max;
|
|
8925
|
+
const pageSizeDefault = declaredMax ?? pageSize;
|
|
8926
|
+
const pageFetchExpr = (variablesExpr) => fetchCall.kind === "gql"
|
|
8927
|
+
? `getGql(context.baseUrl)(${fetchCall.gqlOperationNameExpr}, ${fetchCall.queryConstName}, ${variablesExpr})`
|
|
8928
|
+
: `httpClient(\`\${context.baseUrl}${fetchCall.endpointPath}\`, { method: "POST", body: JSON.stringify(${variablesExpr}) })`;
|
|
8744
8929
|
const countKeyExpr = isValidJsIdentifier(countKey) ? countKey : JSON.stringify(countKey);
|
|
8745
8930
|
const skipKeyExpr = isValidJsIdentifier(skipKey) ? skipKey : JSON.stringify(skipKey);
|
|
8746
8931
|
const paginationBase = pathAccessExpr("baseVariables", containerPath);
|
|
@@ -8753,32 +8938,55 @@ function buildPaginatedGqlExecuteHttpBody(opts) {
|
|
|
8753
8938
|
// `lastPage` is typed as ${pascal}Response (never null): the first page is
|
|
8754
8939
|
// fetched before the loop starts, so there's nothing left to narrow.
|
|
8755
8940
|
const withItemsOverrideExpr = buildNestedSpreadOverride("lastPage", arrayPath, "[...itemsById.values()]");
|
|
8756
|
-
//
|
|
8757
|
-
//
|
|
8758
|
-
//
|
|
8759
|
-
//
|
|
8760
|
-
|
|
8941
|
+
// The server's own reported total is left untouched — overwriting it with
|
|
8942
|
+
// the delivered count would make a MAX_PAGES-capped fetch indistinguishable
|
|
8943
|
+
// from a genuinely complete one. `deliveredCount`/`truncated` are added as
|
|
8944
|
+
// siblings on the response so a caller can tell "total=5000, delivered=100,
|
|
8945
|
+
// truncated=true" apart from "total=436, delivered=436, truncated=false".
|
|
8946
|
+
const withTotalOverrideExpr = `{ ...withItems, deliveredCount: itemsById.size, truncated }`;
|
|
8761
8947
|
return ` const baseVariables = ${gqlVariablesExpr};
|
|
8762
|
-
const PAGE_SIZE = ${
|
|
8948
|
+
const PAGE_SIZE = payload.pageSize ?? ${pageSizeDefault};
|
|
8763
8949
|
// Bounded so a paging bug (a total that never converges) can't loop forever.
|
|
8764
8950
|
const MAX_PAGES = payload.maxPages ?? 50;
|
|
8765
8951
|
const itemsById = new Map<string, ${itemTypeExpr}>();
|
|
8766
8952
|
let skip = 0;
|
|
8767
|
-
const page = await
|
|
8953
|
+
const page = await ${pageFetchExpr(variablesForCall)};
|
|
8768
8954
|
let lastPage: ${pascal}Response = page;
|
|
8769
8955
|
let total = ${totalAccessExpr};
|
|
8770
|
-
|
|
8956
|
+
const firstPageItems = ${arrayAccessExpr};
|
|
8957
|
+
for (const item of firstPageItems) {
|
|
8771
8958
|
itemsById.set(String(${identityAccessExpr}), item);
|
|
8772
8959
|
}
|
|
8773
8960
|
skip += PAGE_SIZE;
|
|
8774
|
-
|
|
8775
|
-
|
|
8961
|
+
// A first page shorter than what was asked for is the same "nothing left"
|
|
8962
|
+
// signal as a short subsequent page — stop before ever entering the loop
|
|
8963
|
+
// instead of issuing a request the server already told us would come
|
|
8964
|
+
// back empty.
|
|
8965
|
+
const firstPageWasShort = firstPageItems.length < PAGE_SIZE;
|
|
8966
|
+
for (
|
|
8967
|
+
let pageIndex = 1;
|
|
8968
|
+
!firstPageWasShort && pageIndex < MAX_PAGES && itemsById.size < total;
|
|
8969
|
+
pageIndex++
|
|
8970
|
+
) {
|
|
8971
|
+
const page = await ${pageFetchExpr(variablesForCall)};
|
|
8776
8972
|
lastPage = page;
|
|
8777
8973
|
total = ${totalAccessExpr};
|
|
8778
|
-
|
|
8974
|
+
const sizeBeforePage = itemsById.size;
|
|
8975
|
+
const pageItems = ${arrayAccessExpr};
|
|
8976
|
+
for (const item of pageItems) {
|
|
8779
8977
|
itemsById.set(String(${identityAccessExpr}), item);
|
|
8780
8978
|
}
|
|
8979
|
+
// A server-reported total that doesn't exactly match the count of
|
|
8980
|
+
// distinct items actually returned would otherwise drive the loop to
|
|
8981
|
+
// MAX_PAGES worth of wasted empty requests; stop as soon as a page
|
|
8982
|
+
// contributes nothing new.
|
|
8983
|
+
if (itemsById.size === sizeBeforePage) break;
|
|
8781
8984
|
skip += PAGE_SIZE;
|
|
8985
|
+
// A page shorter than what was asked for is the server's own signal
|
|
8986
|
+
// that nothing is left, whether or not its reported total agrees —
|
|
8987
|
+
// stop here instead of issuing a request the server already told us
|
|
8988
|
+
// would come back empty.
|
|
8989
|
+
if (pageItems.length < PAGE_SIZE) break;
|
|
8782
8990
|
}
|
|
8783
8991
|
${foldMergeLines.length > 0 ? `${foldMergeLines.join("\n")}\n` : ""} const truncated = itemsById.size < total;
|
|
8784
8992
|
const withItems = ${withItemsOverrideExpr};
|
|
@@ -8826,7 +9034,21 @@ function buildContractChecklist(opts) {
|
|
|
8826
9034
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
8827
9035
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
8828
9036
|
function emitContractTs(opts) {
|
|
8829
|
-
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, payloadFieldNames, optionalPayloadFieldNames = new Set(), headerBindings = [], unpopulatedDeclaredVariables = [], } = opts;
|
|
9037
|
+
const { siteId, displayName, pascal, baseUrl, baseHeaders, minTime, safeRps, hasRateLimitProbeData = false, responseBody, responseBodySamples = [responseBody], gql, gqlQuery, endpointPath, gqlOperationName, gqlVariables, allCaptures = [], auxFiles, multiStepBody, omitExecuteHttp = false, isSubmissionFlow = false, inputBody, hasMultipartStep = false, actionSteps = [], foldReturnSpec = null, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, discoveredStructuredKeys, valueConstraints = value_constraints_1.EMPTY_VALUE_CONSTRAINTS, payloadFieldNames, optionalPayloadFieldNames = new Set(), headerBindings = [], unpopulatedDeclaredVariables = [], fallbackGateSpec = {}, } = opts;
|
|
9038
|
+
const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
|
|
9039
|
+
/** Rendered `meta.browserFallbackGate` literal: `false` verbatim, a
|
|
9040
|
+
* list-derived arrow-function predicate matching against `error.name`, or
|
|
9041
|
+
* "" (omitted) when the flow declared no gate — preserving byte-identical
|
|
9042
|
+
* output for every flow that doesn't opt in. */
|
|
9043
|
+
const browserFallbackGateLiteral = browserFallbackGate === false
|
|
9044
|
+
? "\n browserFallbackGate: false,"
|
|
9045
|
+
: browserFallbackGate !== undefined
|
|
9046
|
+
? `\n browserFallbackGate: (error) => ${JSON.stringify(browserFallbackGate)}.includes(error.name),`
|
|
9047
|
+
: "";
|
|
9048
|
+
/** Rendered `createHttpClient({ ..., defaultTimeoutMs })` fragment — "" when
|
|
9049
|
+
* the flow declared no `httpTimeoutMs`, so the call is byte-identical to
|
|
9050
|
+
* today's when the key is absent. */
|
|
9051
|
+
const defaultTimeoutMsOption = httpTimeoutMs !== undefined ? `, defaultTimeoutMs: ${httpTimeoutMs}` : "";
|
|
8830
9052
|
// This is the CLIENT-level schema — createHttpClient's default, and the
|
|
8831
9053
|
// plugin's caller-facing contract (what executeHttp's return value promises
|
|
8832
9054
|
// its own caller). It does NOT validate any individual call in a multi-step
|
|
@@ -8915,11 +9137,20 @@ function emitContractTs(opts) {
|
|
|
8915
9137
|
: inputBody
|
|
8916
9138
|
? `z.object({})`
|
|
8917
9139
|
: `z.object({\n query: z.string().min(1),\n})`;
|
|
8918
|
-
// Only the single-endpoint
|
|
9140
|
+
// Only the single-endpoint read path (a real primary operation, no
|
|
8919
9141
|
// multi-step flow) is a candidate for a paging signal — multiStepBody
|
|
8920
|
-
// already owns its own per-call semantics.
|
|
8921
|
-
|
|
8922
|
-
|
|
9142
|
+
// already owns its own per-call semantics. A REST primary has no
|
|
9143
|
+
// operationName/query, so it already collapses to the same identity
|
|
9144
|
+
// operationGroupKey falls back to for such a capture
|
|
9145
|
+
// (`${endpointPath}::anonymous`) — this is the same identity
|
|
9146
|
+
// detectPaginationSignal uses to match sibling captures below.
|
|
9147
|
+
const paginationOperationIdentity = gql
|
|
9148
|
+
? gqlOperationName
|
|
9149
|
+
? `${endpointPath}::${gqlOperationName}`
|
|
9150
|
+
: null
|
|
9151
|
+
: `${endpointPath}::anonymous`;
|
|
9152
|
+
const paginationSignal = !multiStepBody && paginationOperationIdentity
|
|
9153
|
+
? detectPaginationSignal(responseBody, gqlVariables, paginationOperationIdentity, allCaptures)
|
|
8923
9154
|
: null;
|
|
8924
9155
|
// A resolved drill-down fold plan on the single-primary getGql/httpClient
|
|
8925
9156
|
// hot path (`multiStepBody` unset — see emitMultiStepExecuteHttp for the
|
|
@@ -8929,7 +9160,7 @@ function emitContractTs(opts) {
|
|
|
8929
9160
|
// drop the fold feature the flow author declared (see
|
|
8930
9161
|
// recon-generate-foldreturn-regresses-primary-op-and-payload-to-ats-submission-shape.md).
|
|
8931
9162
|
// Also threaded additively into `paginationSignal`'s fetch loop below (see
|
|
8932
|
-
//
|
|
9163
|
+
// buildPaginatedFetchLoopExecuteHttpBody) — the fold runs against the final
|
|
8933
9164
|
// assembled/de-duplicated page items, not just the first page's captured
|
|
8934
9165
|
// sample, so a paginated primary is no longer excluded from folding.
|
|
8935
9166
|
const singlePrimaryFoldPlans = resolveApplicableFoldPlans(actionSteps, foldReturnSpec, multiStepBody);
|
|
@@ -8970,12 +9201,13 @@ function emitContractTs(opts) {
|
|
|
8970
9201
|
const addExtendField = (name, line) => {
|
|
8971
9202
|
extendFields.set(name, line);
|
|
8972
9203
|
};
|
|
8973
|
-
// A detected bounded-paging signal means
|
|
9204
|
+
// A detected bounded-paging signal means buildPaginatedFetchLoopExecuteHttpBody
|
|
8974
9205
|
// will emit a loop bounded by MAX_PAGES — expose that bound as a caller-
|
|
8975
9206
|
// overridable payload field, mirroring how PAGE_SIZE is already sourced
|
|
8976
9207
|
// from the detected signal.
|
|
8977
9208
|
if (paginationSignal) {
|
|
8978
9209
|
addExtendField("maxPages", " maxPages: z.number().int().positive().optional(),");
|
|
9210
|
+
addExtendField("pageSize", " pageSize: z.number().int().positive().optional(),");
|
|
8979
9211
|
}
|
|
8980
9212
|
// The base extend's own keys — job-application submission flows only.
|
|
8981
9213
|
if (usesApplicantContactSchema) {
|
|
@@ -9084,18 +9316,27 @@ function emitContractTs(opts) {
|
|
|
9084
9316
|
? [...discoveredAdditionalBodyKeys.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
9085
9317
|
: [];
|
|
9086
9318
|
let usesMultipartBoolean = false;
|
|
9087
|
-
for (const [name,
|
|
9319
|
+
for (const [name, info] of sortedAdditionalKeys) {
|
|
9088
9320
|
if (isReservedByApplicantContactSchema(name))
|
|
9089
9321
|
continue;
|
|
9090
9322
|
// Use multipartBoolean() for booleans when multipart is in play, so
|
|
9091
9323
|
// multipart string-encoded "true"/"false" round-trip to native booleans
|
|
9092
9324
|
// (matches the inputBody boolean handling for parity).
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9325
|
+
// A closed-set vocabulary (string) or a discovered capacity ceiling
|
|
9326
|
+
// (number) narrows the caller-facing schema instead of falling through
|
|
9327
|
+
// to the unconstrained default — see {@link AdditionalBodyKeyInfo}.
|
|
9328
|
+
const zod = info.kind === "string"
|
|
9329
|
+
? info.enumValues
|
|
9330
|
+
? `z.enum([${info.enumValues.map((v) => JSON.stringify(v)).join(", ")}])`
|
|
9331
|
+
: "z.string()"
|
|
9332
|
+
: info.kind === "number"
|
|
9333
|
+
? info.capacityMax !== undefined
|
|
9334
|
+
? payloadNeedsMultipart
|
|
9335
|
+
? `z.coerce.number().max(${info.capacityMax})`
|
|
9336
|
+
: `z.number().max(${info.capacityMax})`
|
|
9337
|
+
: payloadNeedsMultipart
|
|
9338
|
+
? "z.coerce.number()"
|
|
9339
|
+
: "z.number()"
|
|
9099
9340
|
: payloadNeedsMultipart
|
|
9100
9341
|
? "multipartBoolean()"
|
|
9101
9342
|
: "z.boolean()";
|
|
@@ -9182,6 +9423,32 @@ function emitContractTs(opts) {
|
|
|
9182
9423
|
extendFields.set(fieldName, line.replace(/,\s*$/, ".optional(),"));
|
|
9183
9424
|
}
|
|
9184
9425
|
}
|
|
9426
|
+
// A consumer-declared value constraint overrides its matching field's
|
|
9427
|
+
// emitted Zod expression here, at the single merge point every discovery
|
|
9428
|
+
// source above funnels through — see {@link ReconValueConstraints}. A
|
|
9429
|
+
// declared field name with no entry in extendFields never appeared in any
|
|
9430
|
+
// capture this run and is a no-op, not an error: there is no schema line
|
|
9431
|
+
// for it to attach to.
|
|
9432
|
+
for (const [fieldName, constraint] of Object.entries(valueConstraints)) {
|
|
9433
|
+
const line = extendFields.get(fieldName);
|
|
9434
|
+
if (line === null || line === undefined)
|
|
9435
|
+
continue;
|
|
9436
|
+
if (constraint.enumValues === undefined &&
|
|
9437
|
+
constraint.min === undefined &&
|
|
9438
|
+
constraint.max === undefined) {
|
|
9439
|
+
continue;
|
|
9440
|
+
}
|
|
9441
|
+
const key = isValidJsIdentifier(fieldName) ? fieldName : JSON.stringify(fieldName);
|
|
9442
|
+
const zod = constraint.enumValues
|
|
9443
|
+
? `z.enum([${constraint.enumValues.map((v) => JSON.stringify(v)).join(", ")}])`
|
|
9444
|
+
: [
|
|
9445
|
+
"z.number()",
|
|
9446
|
+
...(constraint.min !== undefined ? [`.min(${constraint.min})`] : []),
|
|
9447
|
+
...(constraint.max !== undefined ? [`.max(${constraint.max})`] : []),
|
|
9448
|
+
].join("");
|
|
9449
|
+
const optional = line.trimEnd().endsWith(".optional(),");
|
|
9450
|
+
extendFields.set(fieldName, ` ${key}: ${zod}${optional ? ".optional()" : ""},`);
|
|
9451
|
+
}
|
|
9185
9452
|
const mergedExtension = extendFields.size > 0 ? `.extend({\n${[...extendFields.values()].join("\n")}\n})` : "";
|
|
9186
9453
|
const payloadSchemaExpr = `${basePayloadSchemaExpr}${mergedExtension}`;
|
|
9187
9454
|
// basePayloadSchemaExpr's own Answers field always wraps in
|
|
@@ -9261,18 +9528,24 @@ ${
|
|
|
9261
9528
|
// GraphQL to the primary endpoint.
|
|
9262
9529
|
needsFoldHttpClient
|
|
9263
9530
|
? `
|
|
9264
|
-
const httpClient = createHttpClient({ schema: z.unknown(), bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
|
|
9531
|
+
const httpClient = createHttpClient({ schema: z.unknown(), bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)}${defaultTimeoutMsOption} });
|
|
9265
9532
|
`
|
|
9266
9533
|
: ""}`
|
|
9267
9534
|
: `
|
|
9268
|
-
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
|
|
9535
|
+
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)}${defaultTimeoutMsOption} });
|
|
9269
9536
|
`;
|
|
9270
9537
|
const gqlOperationNameExpr = gqlOperationName
|
|
9271
9538
|
? JSON.stringify(gqlOperationName)
|
|
9272
9539
|
: JSON.stringify(`${pascal}Search`);
|
|
9540
|
+
// A REST primary with a detected paging signal renders `baseVariables` from
|
|
9541
|
+
// the same captured request-variables object the signal was itself
|
|
9542
|
+
// detected from (see paginationOperationIdentity above) — the default
|
|
9543
|
+
// `{ q: payload.query }` REST body has no skip/count container to advance.
|
|
9273
9544
|
const gqlVariablesExpr = gqlOperationName
|
|
9274
9545
|
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
9275
|
-
:
|
|
9546
|
+
: paginationSignal
|
|
9547
|
+
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
9548
|
+
: "{ q: payload.query }";
|
|
9276
9549
|
/** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
|
|
9277
9550
|
* — that fold every resolved plan's drill-down data onto `dataVarName`'s
|
|
9278
9551
|
* primary array — the single-primary counterpart of
|
|
@@ -9338,6 +9611,14 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9338
9611
|
// Every target's join/merge — plus any non-hoistable target's own
|
|
9339
9612
|
// chain fetch — goes here, spliced inside the item loop.
|
|
9340
9613
|
const itemScopedLines = [];
|
|
9614
|
+
// True once any target's own chain fetch ends up item-scoped — see
|
|
9615
|
+
// emitMultiStepExecuteHttp's identical flag and emitItemLoopLines.
|
|
9616
|
+
let hasItemScopedFetch = false;
|
|
9617
|
+
// True once any item-scoped target's chain threads a response header
|
|
9618
|
+
// through createHttpClient's shared `bind` store — see
|
|
9619
|
+
// emitMultiStepExecuteHttp's identical flag for why that disqualifies
|
|
9620
|
+
// parallelizing the item loop.
|
|
9621
|
+
let hasItemScopedHeaderThreading = false;
|
|
9341
9622
|
// Word-boundary match — see emitMultiStepExecuteHttp's identical
|
|
9342
9623
|
// `itemVarRefPattern` for why an anchored `${itemVar` pattern misses a
|
|
9343
9624
|
// nested field path's `${(itemVar.field as Record<string,
|
|
@@ -9485,10 +9766,20 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9485
9766
|
// treated as item-scoped too — flat folds must keep emitting
|
|
9486
9767
|
// byte-identical code.
|
|
9487
9768
|
let referencesItemVar = ancestorVars.length === 0;
|
|
9769
|
+
// See emitMultiStepExecuteHttp's identical `chainUsesHeaderThreading`
|
|
9770
|
+
// for why any header-kind produce disqualifies parallelizing the
|
|
9771
|
+
// item loop — httpClient's shared `bind` store applies to every call
|
|
9772
|
+
// this function's own chainLines make too, even though (unlike
|
|
9773
|
+
// emitMultiStepExecuteHttp) this codegen path never renders a local
|
|
9774
|
+
// variable for the produced header itself.
|
|
9775
|
+
let chainUsesHeaderThreading = false;
|
|
9488
9776
|
for (const chainIndex of target.chain) {
|
|
9489
9777
|
const chainStep = actionSteps[chainIndex];
|
|
9490
9778
|
if (!chainStep)
|
|
9491
9779
|
continue;
|
|
9780
|
+
if (chainStep.produces.some((p) => p.kind === "header")) {
|
|
9781
|
+
chainUsesHeaderThreading = true;
|
|
9782
|
+
}
|
|
9492
9783
|
// The zero-variance guard lives inside `parameterizeUrl` itself
|
|
9493
9784
|
// (see above) so it can skip only the threaded-value splice while
|
|
9494
9785
|
// still letting a spec-declared drillParamBindings substitution
|
|
@@ -9510,13 +9801,17 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9510
9801
|
const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
|
|
9511
9802
|
if (referencesItemVar) {
|
|
9512
9803
|
itemScopedLines.push(...chainLines, ...matchLines);
|
|
9804
|
+
if (chainLines.length > 0)
|
|
9805
|
+
hasItemScopedFetch = true;
|
|
9806
|
+
if (chainUsesHeaderThreading)
|
|
9807
|
+
hasItemScopedHeaderThreading = true;
|
|
9513
9808
|
}
|
|
9514
9809
|
else {
|
|
9515
9810
|
hoistedChainLines.push(...chainLines);
|
|
9516
9811
|
itemScopedLines.push(...matchLines);
|
|
9517
9812
|
}
|
|
9518
9813
|
}
|
|
9519
|
-
lines.push(...hoistedChainLines, ...itemOpenLines,
|
|
9814
|
+
lines.push(...hoistedChainLines, ...emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch && !hasItemScopedHeaderThreading), ...ancestorCloseLines, "");
|
|
9520
9815
|
}
|
|
9521
9816
|
return lines;
|
|
9522
9817
|
};
|
|
@@ -9535,13 +9830,19 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9535
9830
|
const executeHttpBody = multiStepBody
|
|
9536
9831
|
? multiStepBody
|
|
9537
9832
|
: paginationSignal
|
|
9538
|
-
?
|
|
9833
|
+
? buildPaginatedFetchLoopExecuteHttpBody({
|
|
9539
9834
|
pascal,
|
|
9540
|
-
gqlOperationNameExpr,
|
|
9541
|
-
queryConstName: `${pascal.toUpperCase()}_QUERY`,
|
|
9542
9835
|
gqlVariablesExpr,
|
|
9543
9836
|
signal: paginationSignal,
|
|
9544
9837
|
foldMergeLines: paginatedFoldMergeLines,
|
|
9838
|
+
valueConstraints,
|
|
9839
|
+
fetchCall: gql
|
|
9840
|
+
? {
|
|
9841
|
+
kind: "gql",
|
|
9842
|
+
gqlOperationNameExpr,
|
|
9843
|
+
queryConstName: `${pascal.toUpperCase()}_QUERY`,
|
|
9844
|
+
}
|
|
9845
|
+
: { kind: "rest", endpointPath },
|
|
9545
9846
|
})
|
|
9546
9847
|
: gql
|
|
9547
9848
|
? ` const data = await getGql(context.baseUrl)(${gqlOperationNameExpr}, ${pascal.toUpperCase()}_QUERY, ${gqlVariablesExpr});
|
|
@@ -9658,7 +9959,7 @@ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
|
9658
9959
|
// since the multipart wire format is what makes that field's
|
|
9659
9960
|
// JSON-stringified encoding parseable.
|
|
9660
9961
|
`
|
|
9661
|
-
: ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}
|
|
9962
|
+
: ""}apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${payloadNeedsMultipart || usesApplicantContactSchema || hasMultipartStep ? "\n multipart: true," : ""}${browserFallbackGateLiteral}
|
|
9662
9963
|
},
|
|
9663
9964
|
${executeHttpMethodBlock}
|
|
9664
9965
|
/** Browser fallback: Stagehand + Steel — invoked only when hot path fails. */
|
|
@@ -10198,6 +10499,21 @@ async function resolveFormSchema(specifier) {
|
|
|
10198
10499
|
logger.info(`form-schema: ${specifier === load_form_schema_1.FORM_SCHEMA_NONE ? "none (no ATS form recovery)" : `custom keys from ${specifier}`}`);
|
|
10199
10500
|
return formSchema;
|
|
10200
10501
|
}
|
|
10502
|
+
/**
|
|
10503
|
+
* Resolves the consumer-declared value constraints for this run.
|
|
10504
|
+
*
|
|
10505
|
+
* Absent `--value-constraints`, no override happens: every field's emitted
|
|
10506
|
+
* Zod expression stays whatever its discovery mechanism (Phase E/F options,
|
|
10507
|
+
* form-schema fields, structured keys) already inferred — the same "absence
|
|
10508
|
+
* means none" discipline `--vocabulary`/`--form-schema` use.
|
|
10509
|
+
*/
|
|
10510
|
+
async function resolveValueConstraints(specifier) {
|
|
10511
|
+
if (!specifier)
|
|
10512
|
+
return value_constraints_1.EMPTY_VALUE_CONSTRAINTS;
|
|
10513
|
+
const valueConstraints = await (0, load_value_constraints_1.loadReconValueConstraints)(specifier, process.cwd());
|
|
10514
|
+
logger.info(`value-constraints: ${specifier === load_value_constraints_1.VALUE_CONSTRAINTS_NONE ? "none (no field overrides)" : `custom constraints from ${specifier}`}`);
|
|
10515
|
+
return valueConstraints;
|
|
10516
|
+
}
|
|
10201
10517
|
/**
|
|
10202
10518
|
* The capture whose own response `assertRequiredUrlFieldsReferenced`'s
|
|
10203
10519
|
* scan actually describes: a submission flow returns
|
|
@@ -10362,6 +10678,7 @@ async function main() {
|
|
|
10362
10678
|
let emit = "ts";
|
|
10363
10679
|
let vocabularySpecifier = "";
|
|
10364
10680
|
let formSchemaSpecifier = "";
|
|
10681
|
+
let valueConstraintsSpecifier = "";
|
|
10365
10682
|
let runDir;
|
|
10366
10683
|
let allowEmptyCapture = false;
|
|
10367
10684
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -10371,6 +10688,8 @@ async function main() {
|
|
|
10371
10688
|
vocabularySpecifier = args[++i];
|
|
10372
10689
|
else if (args[i] === "--form-schema" && args[i + 1])
|
|
10373
10690
|
formSchemaSpecifier = args[++i];
|
|
10691
|
+
else if (args[i] === "--value-constraints" && args[i + 1])
|
|
10692
|
+
valueConstraintsSpecifier = args[++i];
|
|
10374
10693
|
else if (args[i] === "--run-dir" && args[i + 1])
|
|
10375
10694
|
runDir = args[++i];
|
|
10376
10695
|
else if (args[i] === "--force")
|
|
@@ -10440,7 +10759,7 @@ async function main() {
|
|
|
10440
10759
|
return [];
|
|
10441
10760
|
}
|
|
10442
10761
|
})();
|
|
10443
|
-
const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, } = (() => {
|
|
10762
|
+
const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, fallbackGateSpec, } = (() => {
|
|
10444
10763
|
const flowFileContents = (() => {
|
|
10445
10764
|
try {
|
|
10446
10765
|
return (0, node_fs_1.readFileSync)(flowFile, "utf8");
|
|
@@ -10453,6 +10772,10 @@ async function main() {
|
|
|
10453
10772
|
// valid `foldReturn` still resolves when the rest of the flow file is
|
|
10454
10773
|
// degenerate — the two declarations fail independently.
|
|
10455
10774
|
const foldReturnSpec = flowFileContents === null ? null : parseFoldReturnSpec(flowFileContents);
|
|
10775
|
+
// Same independence rule as `foldReturnSpec` above: an empty spec
|
|
10776
|
+
// (missing/malformed declaration) is indistinguishable from "not
|
|
10777
|
+
// declared" and preserves today's byte-identical output.
|
|
10778
|
+
const fallbackGateSpec = flowFileContents === null ? {} : parseFallbackGateSpec(flowFileContents);
|
|
10456
10779
|
try {
|
|
10457
10780
|
const raw = flowFileContents === null ? null : JSON.parse(flowFileContents);
|
|
10458
10781
|
if (Array.isArray(raw))
|
|
@@ -10464,6 +10787,7 @@ async function main() {
|
|
|
10464
10787
|
requireSubmitEndpointMatch: false,
|
|
10465
10788
|
displayName: undefined,
|
|
10466
10789
|
foldReturnSpec,
|
|
10790
|
+
fallbackGateSpec,
|
|
10467
10791
|
};
|
|
10468
10792
|
if (raw !== null &&
|
|
10469
10793
|
typeof raw === "object" &&
|
|
@@ -10478,6 +10802,7 @@ async function main() {
|
|
|
10478
10802
|
requireSubmitEndpointMatch: obj.requireSubmitEndpointMatch ?? false,
|
|
10479
10803
|
displayName: obj.displayName,
|
|
10480
10804
|
foldReturnSpec,
|
|
10805
|
+
fallbackGateSpec,
|
|
10481
10806
|
};
|
|
10482
10807
|
}
|
|
10483
10808
|
return {
|
|
@@ -10488,6 +10813,7 @@ async function main() {
|
|
|
10488
10813
|
requireSubmitEndpointMatch: false,
|
|
10489
10814
|
displayName: undefined,
|
|
10490
10815
|
foldReturnSpec,
|
|
10816
|
+
fallbackGateSpec,
|
|
10491
10817
|
};
|
|
10492
10818
|
}
|
|
10493
10819
|
catch {
|
|
@@ -10499,6 +10825,7 @@ async function main() {
|
|
|
10499
10825
|
requireSubmitEndpointMatch: false,
|
|
10500
10826
|
displayName: undefined,
|
|
10501
10827
|
foldReturnSpec,
|
|
10828
|
+
fallbackGateSpec,
|
|
10502
10829
|
};
|
|
10503
10830
|
}
|
|
10504
10831
|
})();
|
|
@@ -10516,6 +10843,10 @@ async function main() {
|
|
|
10516
10843
|
// Consumer-supplied wire keys for ATS form-schema recovery, or null. When
|
|
10517
10844
|
// null the recovery functions no-op — the engine hardcodes no vendor format.
|
|
10518
10845
|
const formSchema = await resolveFormSchema(formSchemaSpecifier);
|
|
10846
|
+
// Consumer-declared domain facts (enum/min/max) that no captured response
|
|
10847
|
+
// leaf exposes a shape for, or EMPTY_VALUE_CONSTRAINTS when absent — see
|
|
10848
|
+
// {@link ReconValueConstraints}.
|
|
10849
|
+
const valueConstraints = await resolveValueConstraints(valueConstraintsSpecifier);
|
|
10519
10850
|
const pascal = toPascalCase(siteId);
|
|
10520
10851
|
// Read the flow's declared own-backend hosts BEFORE deriving baseUrl, so
|
|
10521
10852
|
// deriveBaseUrl itself can gate on them: a third-party host that happens
|
|
@@ -10937,7 +11268,12 @@ async function main() {
|
|
|
10937
11268
|
parsedOperationName(primaryGraphQLOperation.capture.query ?? ""))
|
|
10938
11269
|
: (fallbackGraphQLCapture?.operationName ??
|
|
10939
11270
|
parsedOperationName(fallbackGraphQLCapture?.query ?? "")),
|
|
10940
|
-
|
|
11271
|
+
// For a REST primary (no primaryGraphQLOperation), the winning
|
|
11272
|
+
// capture's own `variables`/`decodedParams` is the REST equivalent of
|
|
11273
|
+
// a GraphQL operation's captured variables — the object
|
|
11274
|
+
// detectPaginationSignal walks to find a skip/count container.
|
|
11275
|
+
gqlVariables: primaryGraphQLOperation?.capture.variables ??
|
|
11276
|
+
(!gql ? (winningCapture?.variables ?? winningCapture?.decodedParams ?? null) : null),
|
|
10941
11277
|
allCaptures: activeCaptures,
|
|
10942
11278
|
auxFiles,
|
|
10943
11279
|
multiStepBody,
|
|
@@ -10953,10 +11289,12 @@ async function main() {
|
|
|
10953
11289
|
discoveredRawOptionFields,
|
|
10954
11290
|
discoveredAdditionalBodyKeys,
|
|
10955
11291
|
discoveredStructuredKeys,
|
|
11292
|
+
valueConstraints,
|
|
10956
11293
|
payloadFieldNames: browserFlow.payloadFieldNames,
|
|
10957
11294
|
optionalPayloadFieldNames: browserFlow.optionalPayloadFieldNames,
|
|
10958
11295
|
headerBindings,
|
|
10959
11296
|
unpopulatedDeclaredVariables: primaryGraphQLOperation?.unpopulatedDeclaredVariables ?? [],
|
|
11297
|
+
fallbackGateSpec,
|
|
10960
11298
|
};
|
|
10961
11299
|
const contractCode = emitContractTs(contractOpts);
|
|
10962
11300
|
return {
|