@enricai/barnacle 1.12.52 → 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-multicall-fixture.d.ts +156 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +489 -0
- package/dist/scripts/recon-generate-multicall-fixture.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 +514 -110
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-summarize.d.ts +11 -1
- package/dist/scripts/recon-summarize.d.ts.map +1 -1
- package/dist/scripts/recon-summarize.js +51 -32
- package/dist/scripts/recon-summarize.js.map +1 -1
- package/dist/scripts/smoke-test.d.ts +20 -1
- package/dist/scripts/smoke-test.d.ts.map +1 -1
- package/dist/scripts/smoke-test.js +13 -4
- package/dist/scripts/smoke-test.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
|
|
@@ -5764,23 +5887,10 @@ pascalName = null) {
|
|
|
5764
5887
|
// find it. Rebinding by structure, not value, lets referencesItemVar
|
|
5765
5888
|
// (below) correctly stay false so the fetch hoists above the
|
|
5766
5889
|
// per-item loop instead of pinning to it.
|
|
5767
|
-
//
|
|
5768
|
-
//
|
|
5769
|
-
//
|
|
5770
|
-
|
|
5771
|
-
// literal search must still key off the item's own field (that's
|
|
5772
|
-
// the value textually present), while the emitted accessor points
|
|
5773
|
-
// at the ancestor's structurally-corresponding field instead —
|
|
5774
|
-
// searching for the ANCESTOR field's (different) value would never
|
|
5775
|
-
// match anything in `text` and silently freeze the literal.
|
|
5776
|
-
const threadedFieldPairs = isAncestorScoped
|
|
5777
|
-
? rawThreadedFields.map((tf) => ({
|
|
5778
|
-
valueField: tf,
|
|
5779
|
-
accessorField: tf.varName === itemVar
|
|
5780
|
-
? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
|
|
5781
|
-
: tf,
|
|
5782
|
-
}))
|
|
5783
|
-
: rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
|
|
5890
|
+
// See buildThreadedFieldPairs's doc for why the literal-value
|
|
5891
|
+
// search and the rendered accessor deliberately diverge for a
|
|
5892
|
+
// proven ancestor-scoped rebind.
|
|
5893
|
+
const threadedFieldPairs = buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes);
|
|
5784
5894
|
// Accessor swap first: rewrites an already-templated `${payload.X}`
|
|
5785
5895
|
// reference (from applyPayloadKeyValueSubstitutions) to this field's
|
|
5786
5896
|
// real accessor. Each target (`${payload.X}`) is a unique, fully
|
|
@@ -5788,6 +5898,7 @@ pascalName = null) {
|
|
|
5788
5898
|
// never re-shaped into `${payload.X}` form) can't re-match, so a
|
|
5789
5899
|
// sequential pass here carries none of the reentrancy risk the
|
|
5790
5900
|
// literal-value pass below has.
|
|
5901
|
+
let swapFiredForItemBoundField = false;
|
|
5791
5902
|
const swapped = threadedFieldPairs.reduce((acc, { valueField, accessorField }) => {
|
|
5792
5903
|
const replacement = `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`;
|
|
5793
5904
|
// applyPayloadKeyValueSubstitutions only ever names a payload
|
|
@@ -5801,10 +5912,16 @@ pascalName = null) {
|
|
|
5801
5912
|
// reference behind once the literal value itself has already
|
|
5802
5913
|
// been replaced by the payload-key-value pass.
|
|
5803
5914
|
const lastSegment = valueField.field.split(".").pop();
|
|
5915
|
+
const payloadFieldPlaceholder = `\${payload.${valueField.field}}`;
|
|
5916
|
+
const payloadLastSegmentPlaceholder = `\${payload.${lastSegment}}`;
|
|
5917
|
+
if (accessorField.varName === itemVar &&
|
|
5918
|
+
(acc.includes(payloadFieldPlaceholder) || acc.includes(payloadLastSegmentPlaceholder))) {
|
|
5919
|
+
swapFiredForItemBoundField = true;
|
|
5920
|
+
}
|
|
5804
5921
|
return acc
|
|
5805
|
-
.split(
|
|
5922
|
+
.split(payloadFieldPlaceholder)
|
|
5806
5923
|
.join(replacement)
|
|
5807
|
-
.split(
|
|
5924
|
+
.split(payloadLastSegmentPlaceholder)
|
|
5808
5925
|
.join(replacement);
|
|
5809
5926
|
}, text);
|
|
5810
5927
|
// Literal-value substitution: ONE guarded regex-alternation pass over
|
|
@@ -5827,6 +5944,7 @@ pascalName = null) {
|
|
|
5827
5944
|
: {
|
|
5828
5945
|
value: stringValue,
|
|
5829
5946
|
replacement: `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`,
|
|
5947
|
+
isItemBound: accessorField.varName === itemVar,
|
|
5830
5948
|
};
|
|
5831
5949
|
})
|
|
5832
5950
|
.filter((b) => b !== null);
|
|
@@ -5841,6 +5959,25 @@ pascalName = null) {
|
|
|
5841
5959
|
const filteredValueBindings = isProvenInvariant
|
|
5842
5960
|
? valueBindings.filter((b) => isGenuineVaryingQueryValue(b.value, chainCapture, allCaptures))
|
|
5843
5961
|
: valueBindings;
|
|
5962
|
+
// Ground truth for the hoist gate, computed per FIELD from what
|
|
5963
|
+
// this call actually spliced into the request, rather than
|
|
5964
|
+
// inferred after the fact by re-scanning the fully rendered text
|
|
5965
|
+
// for `itemVar`: a target's OTHER join fields can independently
|
|
5966
|
+
// prove isAncestorScoped and rebind cleanly while THIS field's own
|
|
5967
|
+
// rebind attempt still falls back to `?? tf` (no structurally-
|
|
5968
|
+
// corresponding ancestor field exists for it). Deriving
|
|
5969
|
+
// `referencesItemVar` straight from the same value/placeholder
|
|
5970
|
+
// bindings that feed the splice below — rather than relying
|
|
5971
|
+
// solely on the post-hoc itemVar word-boundary scan — closes the
|
|
5972
|
+
// gap where a fallback accessor's occurrence in the final text
|
|
5973
|
+
// could otherwise be missed. A binding whose value never survives
|
|
5974
|
+
// to `filteredValueBindings` (unresolved, or excluded by the
|
|
5975
|
+
// zero-variance guard) is correctly excluded here too — it never
|
|
5976
|
+
// reaches the rendered request, so it can't leak an out-of-scope
|
|
5977
|
+
// identifier into it.
|
|
5978
|
+
if (swapFiredForItemBoundField || filteredValueBindings.some((b) => b.isItemBound)) {
|
|
5979
|
+
referencesItemVar = true;
|
|
5980
|
+
}
|
|
5844
5981
|
const result = substituteThreadedValues(swapped, filteredValueBindings);
|
|
5845
5982
|
const withDrillParamBindings = applyDrillParamBindings(foldReturnSpec, chainCapture, result);
|
|
5846
5983
|
// The frozen-varying-param safety net exists to catch a
|
|
@@ -5872,8 +6009,26 @@ pascalName = null) {
|
|
|
5872
6009
|
// there is no ancestor loop to hoist into in the first place (a
|
|
5873
6010
|
// flat, single-level fold), so it is treated as item-scoped too —
|
|
5874
6011
|
// existing flat folds must keep emitting byte-identical code.
|
|
5875
|
-
|
|
6012
|
+
// Word-boundary match on the bare identifier — not anchored to
|
|
6013
|
+
// `${itemVar` — because `scopedAccessor`/`unknownValueAccessor`
|
|
6014
|
+
// wraps every non-leaf hop in a `(itemVar.field as Record<string,
|
|
6015
|
+
// unknown>)` cast for a nested field path, so the identifier can
|
|
6016
|
+
// appear as `${(itemVar...` rather than immediately after `${`. An
|
|
6017
|
+
// anchored pattern misses that shape entirely and lets a genuinely
|
|
6018
|
+
// item-scoped reference get hoisted above the item loop, where
|
|
6019
|
+
// `itemVar` is never declared.
|
|
6020
|
+
const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
|
|
5876
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;
|
|
5877
6032
|
for (const chainIndex of target.chain) {
|
|
5878
6033
|
const chainStep = actions[chainIndex];
|
|
5879
6034
|
const chainRendered = rendered[chainIndex];
|
|
@@ -5894,8 +6049,10 @@ pascalName = null) {
|
|
|
5894
6049
|
chainLines.push(` ${joined}`);
|
|
5895
6050
|
chainLines.push(` schema: ${chainRendered.schemaExpr},`, ` })) as Record<string, unknown>;`);
|
|
5896
6051
|
for (const p of chainStep.produces) {
|
|
5897
|
-
if (p.kind === "header")
|
|
6052
|
+
if (p.kind === "header") {
|
|
6053
|
+
chainUsesHeaderThreading = true;
|
|
5898
6054
|
continue;
|
|
6055
|
+
}
|
|
5899
6056
|
if (chainDeclared.has(p.name))
|
|
5900
6057
|
continue;
|
|
5901
6058
|
if (!referencedNames.has(p.name))
|
|
@@ -5909,13 +6066,17 @@ pascalName = null) {
|
|
|
5909
6066
|
const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
|
|
5910
6067
|
if (referencesItemVar) {
|
|
5911
6068
|
itemScopedLines.push(...chainLines, ...matchLines);
|
|
6069
|
+
if (chainLines.length > 0)
|
|
6070
|
+
hasItemScopedFetch = true;
|
|
6071
|
+
if (chainUsesHeaderThreading)
|
|
6072
|
+
hasItemScopedHeaderThreading = true;
|
|
5912
6073
|
}
|
|
5913
6074
|
else {
|
|
5914
6075
|
hoistedChainLines.push(...chainLines);
|
|
5915
6076
|
itemScopedLines.push(...matchLines);
|
|
5916
6077
|
}
|
|
5917
6078
|
}
|
|
5918
|
-
lines.push(...hoistedChainLines, ...itemOpenLines,
|
|
6079
|
+
lines.push(...hoistedChainLines, ...emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch && !hasItemScopedHeaderThreading), ...ancestorCloseLines, "");
|
|
5919
6080
|
continue;
|
|
5920
6081
|
}
|
|
5921
6082
|
// Every other chain step (already fully emitted, inline, by the fold
|
|
@@ -7027,29 +7188,44 @@ function findStructurallyCorrespondingAncestorField(ancestorScopes, field) {
|
|
|
7027
7188
|
}
|
|
7028
7189
|
return null;
|
|
7029
7190
|
}
|
|
7030
|
-
/** Recursively searches `obj` for
|
|
7031
|
-
*
|
|
7032
|
-
*
|
|
7033
|
-
* `obj`'s root through the array's
|
|
7034
|
-
* `["entries", "
|
|
7035
|
-
* as `.entries["
|
|
7036
|
-
* a bracketed literal key or an array index.
|
|
7191
|
+
/** Recursively searches `obj` for an array-valued property holding an
|
|
7192
|
+
* element (itself a plain object) that contains `lastSegment` as one of its
|
|
7193
|
+
* own field paths' trailing segments, returning the full dot-path from
|
|
7194
|
+
* `obj`'s root through the array's index down to that field (e.g.
|
|
7195
|
+
* `["entries", "1", "code"]`) — an accessor {@link pathToAccessor} renders
|
|
7196
|
+
* as `.entries["1"].code`, valid whether the numeric segment is treated as
|
|
7197
|
+
* a bracketed literal key or an array index. Scans every element of a
|
|
7198
|
+
* candidate array, not only the first: a sparse first element (e.g. a
|
|
7199
|
+
* sold-out entry missing an optional nested object that every OTHER sibling
|
|
7200
|
+
* genuinely carries) must not silently defeat a structural correspondence
|
|
7201
|
+
* that a later element in the same array would prove. This applies equally
|
|
7202
|
+
* to the recursive descent into a NESTED array-within-array: it also walks
|
|
7203
|
+
* every sibling element rather than only element `0`, so a sparse first
|
|
7204
|
+
* element one level up can't hide a match that lives in a nested array on a
|
|
7205
|
+
* later sibling. */
|
|
7037
7206
|
function findFieldInFirstArrayElement(obj, lastSegment, path = []) {
|
|
7038
7207
|
for (const [key, value] of Object.entries(obj)) {
|
|
7039
7208
|
const childPath = [...path, key];
|
|
7040
7209
|
if (Array.isArray(value)) {
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7210
|
+
for (let i = 0; i < value.length; i++) {
|
|
7211
|
+
const candidate = value[i];
|
|
7212
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
7213
|
+
continue;
|
|
7214
|
+
const candidateObj = candidate;
|
|
7215
|
+
for (const { path: subPath } of walkItemFieldPaths(candidateObj)) {
|
|
7216
|
+
if (subPath[subPath.length - 1] === lastSegment) {
|
|
7217
|
+
return [...childPath, String(i), ...subPath];
|
|
7218
|
+
}
|
|
7048
7219
|
}
|
|
7049
7220
|
}
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7221
|
+
for (let i = 0; i < value.length; i++) {
|
|
7222
|
+
const candidate = value[i];
|
|
7223
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
7224
|
+
continue;
|
|
7225
|
+
const nested = findFieldInFirstArrayElement(candidate, lastSegment, [...childPath, String(i)]);
|
|
7226
|
+
if (nested)
|
|
7227
|
+
return nested;
|
|
7228
|
+
}
|
|
7053
7229
|
continue;
|
|
7054
7230
|
}
|
|
7055
7231
|
if (value !== null && typeof value === "object") {
|
|
@@ -7060,6 +7236,34 @@ function findFieldInFirstArrayElement(obj, lastSegment, path = []) {
|
|
|
7060
7236
|
}
|
|
7061
7237
|
return null;
|
|
7062
7238
|
}
|
|
7239
|
+
/** Pairs each raw threaded field (whose VALUE is what a drill request's
|
|
7240
|
+
* captured text actually contains — the only thing a literal-value search
|
|
7241
|
+
* can ever find) with the accessor it should render as, shared by
|
|
7242
|
+
* {@link emitMultiStepExecuteHttp} and {@link emitContractTs}'s identical
|
|
7243
|
+
* fold-hoist rebind so the two emitters can never drift on this logic (see
|
|
7244
|
+
* this module's fold-plan investigation notes on the two call sites having
|
|
7245
|
+
* previously diverged). For a proven-ancestor-scoped target these
|
|
7246
|
+
* deliberately diverge: the literal search must still key off the item's own
|
|
7247
|
+
* field (the value textually present in the captured request), while the
|
|
7248
|
+
* emitted accessor points at the ancestor's structurally-corresponding field
|
|
7249
|
+
* instead — searching for the ANCESTOR field's (different) value would never
|
|
7250
|
+
* match anything in the rendered text and silently freeze the literal. When
|
|
7251
|
+
* no structurally-corresponding ancestor field exists, the accessor stays
|
|
7252
|
+
* item-bound (`?? tf`) — safe only because every caller's own itemVar
|
|
7253
|
+
* word-boundary scan over the FINAL rendered text (after this pairing feeds
|
|
7254
|
+
* substitution) is what actually decides whether the target may hoist above
|
|
7255
|
+
* its item loop, not this function. */
|
|
7256
|
+
function buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes) {
|
|
7257
|
+
if (!isAncestorScoped) {
|
|
7258
|
+
return rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
|
|
7259
|
+
}
|
|
7260
|
+
return rawThreadedFields.map((tf) => ({
|
|
7261
|
+
valueField: tf,
|
|
7262
|
+
accessorField: tf.varName === itemVar
|
|
7263
|
+
? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
|
|
7264
|
+
: tf,
|
|
7265
|
+
}));
|
|
7266
|
+
}
|
|
7063
7267
|
/** Every string, numeric, and boolean leaf value present anywhere in a response —
|
|
7064
7268
|
* the set a chained drill-down step's request must overlap with for that
|
|
7065
7269
|
* step to count as depending on this response. Deliberately walks the WHOLE
|
|
@@ -7551,6 +7755,41 @@ function parseFoldReturnSpec(flowFileContents) {
|
|
|
7551
7755
|
return null;
|
|
7552
7756
|
}
|
|
7553
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
|
+
}
|
|
7554
7793
|
/** The JS `typeof` a `drillParamBindings` entry's `default` must match for
|
|
7555
7794
|
* its declared `type` — the same "assert the type, then check the default
|
|
7556
7795
|
* agrees" pattern the rest of {@link parseFoldReturnSpec} uses for its other
|
|
@@ -8659,11 +8898,17 @@ function buildNestedSpreadOverride(base, path, leafExpr) {
|
|
|
8659
8898
|
return `{ ...${base}, ${keyExpr}: ${nested} }`;
|
|
8660
8899
|
}
|
|
8661
8900
|
/**
|
|
8662
|
-
* Emits a bounded paging loop for a read-only
|
|
8663
|
-
*
|
|
8664
|
-
*
|
|
8665
|
-
*
|
|
8666
|
-
*
|
|
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.
|
|
8667
8912
|
*
|
|
8668
8913
|
* `foldMergeLines`, when non-empty, threads a resolved single-primary fold
|
|
8669
8914
|
* plan (see `emitContractTs`'s `singlePrimaryFoldPlans`) additively into the
|
|
@@ -8673,9 +8918,14 @@ function buildNestedSpreadOverride(base, path, leafExpr) {
|
|
|
8673
8918
|
* mutates its item in place (`Object.assign`), which is visible through
|
|
8674
8919
|
* `itemsById`'s own stored references — no re-`set` needed.
|
|
8675
8920
|
*/
|
|
8676
|
-
function
|
|
8677
|
-
const { pascal,
|
|
8921
|
+
function buildPaginatedFetchLoopExecuteHttpBody(opts) {
|
|
8922
|
+
const { pascal, gqlVariablesExpr, signal, foldMergeLines, fetchCall, valueConstraints } = opts;
|
|
8678
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}) })`;
|
|
8679
8929
|
const countKeyExpr = isValidJsIdentifier(countKey) ? countKey : JSON.stringify(countKey);
|
|
8680
8930
|
const skipKeyExpr = isValidJsIdentifier(skipKey) ? skipKey : JSON.stringify(skipKey);
|
|
8681
8931
|
const paginationBase = pathAccessExpr("baseVariables", containerPath);
|
|
@@ -8688,32 +8938,55 @@ function buildPaginatedGqlExecuteHttpBody(opts) {
|
|
|
8688
8938
|
// `lastPage` is typed as ${pascal}Response (never null): the first page is
|
|
8689
8939
|
// fetched before the loop starts, so there's nothing left to narrow.
|
|
8690
8940
|
const withItemsOverrideExpr = buildNestedSpreadOverride("lastPage", arrayPath, "[...itemsById.values()]");
|
|
8691
|
-
//
|
|
8692
|
-
//
|
|
8693
|
-
//
|
|
8694
|
-
//
|
|
8695
|
-
|
|
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 }`;
|
|
8696
8947
|
return ` const baseVariables = ${gqlVariablesExpr};
|
|
8697
|
-
const PAGE_SIZE = ${
|
|
8948
|
+
const PAGE_SIZE = payload.pageSize ?? ${pageSizeDefault};
|
|
8698
8949
|
// Bounded so a paging bug (a total that never converges) can't loop forever.
|
|
8699
8950
|
const MAX_PAGES = payload.maxPages ?? 50;
|
|
8700
8951
|
const itemsById = new Map<string, ${itemTypeExpr}>();
|
|
8701
8952
|
let skip = 0;
|
|
8702
|
-
const page = await
|
|
8953
|
+
const page = await ${pageFetchExpr(variablesForCall)};
|
|
8703
8954
|
let lastPage: ${pascal}Response = page;
|
|
8704
8955
|
let total = ${totalAccessExpr};
|
|
8705
|
-
|
|
8956
|
+
const firstPageItems = ${arrayAccessExpr};
|
|
8957
|
+
for (const item of firstPageItems) {
|
|
8706
8958
|
itemsById.set(String(${identityAccessExpr}), item);
|
|
8707
8959
|
}
|
|
8708
8960
|
skip += PAGE_SIZE;
|
|
8709
|
-
|
|
8710
|
-
|
|
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)};
|
|
8711
8972
|
lastPage = page;
|
|
8712
8973
|
total = ${totalAccessExpr};
|
|
8713
|
-
|
|
8974
|
+
const sizeBeforePage = itemsById.size;
|
|
8975
|
+
const pageItems = ${arrayAccessExpr};
|
|
8976
|
+
for (const item of pageItems) {
|
|
8714
8977
|
itemsById.set(String(${identityAccessExpr}), item);
|
|
8715
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;
|
|
8716
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;
|
|
8717
8990
|
}
|
|
8718
8991
|
${foldMergeLines.length > 0 ? `${foldMergeLines.join("\n")}\n` : ""} const truncated = itemsById.size < total;
|
|
8719
8992
|
const withItems = ${withItemsOverrideExpr};
|
|
@@ -8761,7 +9034,21 @@ function buildContractChecklist(opts) {
|
|
|
8761
9034
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
8762
9035
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
8763
9036
|
function emitContractTs(opts) {
|
|
8764
|
-
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}` : "";
|
|
8765
9052
|
// This is the CLIENT-level schema — createHttpClient's default, and the
|
|
8766
9053
|
// plugin's caller-facing contract (what executeHttp's return value promises
|
|
8767
9054
|
// its own caller). It does NOT validate any individual call in a multi-step
|
|
@@ -8850,11 +9137,20 @@ function emitContractTs(opts) {
|
|
|
8850
9137
|
: inputBody
|
|
8851
9138
|
? `z.object({})`
|
|
8852
9139
|
: `z.object({\n query: z.string().min(1),\n})`;
|
|
8853
|
-
// Only the single-endpoint
|
|
9140
|
+
// Only the single-endpoint read path (a real primary operation, no
|
|
8854
9141
|
// multi-step flow) is a candidate for a paging signal — multiStepBody
|
|
8855
|
-
// already owns its own per-call semantics.
|
|
8856
|
-
|
|
8857
|
-
|
|
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)
|
|
8858
9154
|
: null;
|
|
8859
9155
|
// A resolved drill-down fold plan on the single-primary getGql/httpClient
|
|
8860
9156
|
// hot path (`multiStepBody` unset — see emitMultiStepExecuteHttp for the
|
|
@@ -8864,7 +9160,7 @@ function emitContractTs(opts) {
|
|
|
8864
9160
|
// drop the fold feature the flow author declared (see
|
|
8865
9161
|
// recon-generate-foldreturn-regresses-primary-op-and-payload-to-ats-submission-shape.md).
|
|
8866
9162
|
// Also threaded additively into `paginationSignal`'s fetch loop below (see
|
|
8867
|
-
//
|
|
9163
|
+
// buildPaginatedFetchLoopExecuteHttpBody) — the fold runs against the final
|
|
8868
9164
|
// assembled/de-duplicated page items, not just the first page's captured
|
|
8869
9165
|
// sample, so a paginated primary is no longer excluded from folding.
|
|
8870
9166
|
const singlePrimaryFoldPlans = resolveApplicableFoldPlans(actionSteps, foldReturnSpec, multiStepBody);
|
|
@@ -8905,12 +9201,13 @@ function emitContractTs(opts) {
|
|
|
8905
9201
|
const addExtendField = (name, line) => {
|
|
8906
9202
|
extendFields.set(name, line);
|
|
8907
9203
|
};
|
|
8908
|
-
// A detected bounded-paging signal means
|
|
9204
|
+
// A detected bounded-paging signal means buildPaginatedFetchLoopExecuteHttpBody
|
|
8909
9205
|
// will emit a loop bounded by MAX_PAGES — expose that bound as a caller-
|
|
8910
9206
|
// overridable payload field, mirroring how PAGE_SIZE is already sourced
|
|
8911
9207
|
// from the detected signal.
|
|
8912
9208
|
if (paginationSignal) {
|
|
8913
9209
|
addExtendField("maxPages", " maxPages: z.number().int().positive().optional(),");
|
|
9210
|
+
addExtendField("pageSize", " pageSize: z.number().int().positive().optional(),");
|
|
8914
9211
|
}
|
|
8915
9212
|
// The base extend's own keys — job-application submission flows only.
|
|
8916
9213
|
if (usesApplicantContactSchema) {
|
|
@@ -9019,18 +9316,27 @@ function emitContractTs(opts) {
|
|
|
9019
9316
|
? [...discoveredAdditionalBodyKeys.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
9020
9317
|
: [];
|
|
9021
9318
|
let usesMultipartBoolean = false;
|
|
9022
|
-
for (const [name,
|
|
9319
|
+
for (const [name, info] of sortedAdditionalKeys) {
|
|
9023
9320
|
if (isReservedByApplicantContactSchema(name))
|
|
9024
9321
|
continue;
|
|
9025
9322
|
// Use multipartBoolean() for booleans when multipart is in play, so
|
|
9026
9323
|
// multipart string-encoded "true"/"false" round-trip to native booleans
|
|
9027
9324
|
// (matches the inputBody boolean handling for parity).
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
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()"
|
|
9034
9340
|
: payloadNeedsMultipart
|
|
9035
9341
|
? "multipartBoolean()"
|
|
9036
9342
|
: "z.boolean()";
|
|
@@ -9117,6 +9423,32 @@ function emitContractTs(opts) {
|
|
|
9117
9423
|
extendFields.set(fieldName, line.replace(/,\s*$/, ".optional(),"));
|
|
9118
9424
|
}
|
|
9119
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
|
+
}
|
|
9120
9452
|
const mergedExtension = extendFields.size > 0 ? `.extend({\n${[...extendFields.values()].join("\n")}\n})` : "";
|
|
9121
9453
|
const payloadSchemaExpr = `${basePayloadSchemaExpr}${mergedExtension}`;
|
|
9122
9454
|
// basePayloadSchemaExpr's own Answers field always wraps in
|
|
@@ -9196,18 +9528,24 @@ ${
|
|
|
9196
9528
|
// GraphQL to the primary endpoint.
|
|
9197
9529
|
needsFoldHttpClient
|
|
9198
9530
|
? `
|
|
9199
|
-
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} });
|
|
9200
9532
|
`
|
|
9201
9533
|
: ""}`
|
|
9202
9534
|
: `
|
|
9203
|
-
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} });
|
|
9204
9536
|
`;
|
|
9205
9537
|
const gqlOperationNameExpr = gqlOperationName
|
|
9206
9538
|
? JSON.stringify(gqlOperationName)
|
|
9207
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.
|
|
9208
9544
|
const gqlVariablesExpr = gqlOperationName
|
|
9209
9545
|
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
9210
|
-
:
|
|
9546
|
+
: paginationSignal
|
|
9547
|
+
? renderGqlVariablesExpr(gqlVariables, payloadFieldNames, optionalPayloadFieldNames)
|
|
9548
|
+
: "{ q: payload.query }";
|
|
9211
9549
|
/** Builds the nested `for` loop block(s) — see {@link pathToFoldLoopLines}
|
|
9212
9550
|
* — that fold every resolved plan's drill-down data onto `dataVarName`'s
|
|
9213
9551
|
* primary array — the single-primary counterpart of
|
|
@@ -9273,7 +9611,19 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9273
9611
|
// Every target's join/merge — plus any non-hoistable target's own
|
|
9274
9612
|
// chain fetch — goes here, spliced inside the item loop.
|
|
9275
9613
|
const itemScopedLines = [];
|
|
9276
|
-
|
|
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;
|
|
9622
|
+
// Word-boundary match — see emitMultiStepExecuteHttp's identical
|
|
9623
|
+
// `itemVarRefPattern` for why an anchored `${itemVar` pattern misses a
|
|
9624
|
+
// nested field path's `${(itemVar.field as Record<string,
|
|
9625
|
+
// unknown>)...}` cast wrapper.
|
|
9626
|
+
const itemVarRefPattern = new RegExp(`\\b${itemVar}\\b`);
|
|
9277
9627
|
for (const [targetIndex, target] of foldPlan.targets.entries()) {
|
|
9278
9628
|
const matchedPrimaryItem = primaryItemsWithAncestors[target.primaryMatchedItemIndex];
|
|
9279
9629
|
if (!matchedPrimaryItem) {
|
|
@@ -9343,29 +9693,11 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9343
9693
|
...target.joinFields.map((field) => ({ varName: itemVar, field })),
|
|
9344
9694
|
...findThreadedJoinFields(threadingScopes, chainCapture, allCaptures),
|
|
9345
9695
|
]);
|
|
9346
|
-
// Mirrors emitMultiStepExecuteHttp's identical rebind
|
|
9347
|
-
//
|
|
9348
|
-
//
|
|
9349
|
-
//
|
|
9350
|
-
|
|
9351
|
-
// Pairs each raw threaded field (whose VALUE is what's actually
|
|
9352
|
-
// present in `rawUrl` — the only thing a literal-value search can
|
|
9353
|
-
// ever find) with the accessor it should render as. For an
|
|
9354
|
-
// ancestor-scoped rebind these deliberately diverge: the literal
|
|
9355
|
-
// search must still key off the item's own field (that's the
|
|
9356
|
-
// value textually present in the URL), while the emitted accessor
|
|
9357
|
-
// points at the ancestor's structurally-corresponding field
|
|
9358
|
-
// instead — searching for the ANCESTOR field's (different) value
|
|
9359
|
-
// would never match anything in the URL and silently freeze the
|
|
9360
|
-
// literal.
|
|
9361
|
-
const threadedFieldPairs = isAncestorScoped
|
|
9362
|
-
? rawThreadedFields.map((tf) => ({
|
|
9363
|
-
valueField: tf,
|
|
9364
|
-
accessorField: tf.varName === itemVar
|
|
9365
|
-
? (findStructurallyCorrespondingAncestorField(ancestorScopes, tf.field) ?? tf)
|
|
9366
|
-
: tf,
|
|
9367
|
-
}))
|
|
9368
|
-
: rawThreadedFields.map((tf) => ({ valueField: tf, accessorField: tf }));
|
|
9696
|
+
// Mirrors emitMultiStepExecuteHttp's identical rebind — see
|
|
9697
|
+
// buildThreadedFieldPairs's doc for why the literal-value search
|
|
9698
|
+
// and the rendered accessor deliberately diverge for a proven
|
|
9699
|
+
// ancestor-scoped rebind.
|
|
9700
|
+
const threadedFieldPairs = buildThreadedFieldPairs(isAncestorScoped, rawThreadedFields, itemVar, ancestorScopes);
|
|
9369
9701
|
// ONE guarded regex-alternation pass over `withBase` for every
|
|
9370
9702
|
// threaded field's value, longest first — see substituteThreadedValues's
|
|
9371
9703
|
// doc for why a per-field sequential `.replace()` loop here (the bug
|
|
@@ -9387,6 +9719,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9387
9719
|
: {
|
|
9388
9720
|
value: stringValue,
|
|
9389
9721
|
replacement: `\${${scopedAccessor(accessorField.varName, accessorField.field)}}`,
|
|
9722
|
+
isItemBound: accessorField.varName === itemVar,
|
|
9390
9723
|
};
|
|
9391
9724
|
})
|
|
9392
9725
|
.filter((b) => b !== null);
|
|
@@ -9399,6 +9732,20 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9399
9732
|
const filteredValueBindings = isProvenInvariant
|
|
9400
9733
|
? valueBindings.filter((b) => isGenuineVaryingQueryValue(b.value, chainCapture, allCaptures))
|
|
9401
9734
|
: valueBindings;
|
|
9735
|
+
// Mirrors emitMultiStepExecuteHttp's identical ground-truth hoist
|
|
9736
|
+
// signal: derive `referencesItemVar` straight from the same
|
|
9737
|
+
// value bindings that feed the splice below, rather than relying
|
|
9738
|
+
// solely on the post-hoc itemVar word-boundary scan — a target's
|
|
9739
|
+
// OTHER join fields can independently prove isAncestorScoped and
|
|
9740
|
+
// rebind cleanly while THIS field's own rebind attempt still
|
|
9741
|
+
// falls back to `?? tf` (no structurally-corresponding ancestor
|
|
9742
|
+
// field exists for it). A binding excluded from
|
|
9743
|
+
// `filteredValueBindings` (unresolved, or dropped by the
|
|
9744
|
+
// zero-variance guard) never reaches the rendered URL, so it's
|
|
9745
|
+
// correctly excluded here too.
|
|
9746
|
+
if (filteredValueBindings.some((b) => b.isItemBound)) {
|
|
9747
|
+
referencesItemVar = true;
|
|
9748
|
+
}
|
|
9402
9749
|
const result = substituteThreadedValues(withBase, filteredValueBindings);
|
|
9403
9750
|
const withDrillParamBindings = applyDrillParamBindings(foldReturnSpec, chainCapture, result);
|
|
9404
9751
|
// See emitMultiStepExecuteHttp's identical guard: the frozen-
|
|
@@ -9419,10 +9766,20 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9419
9766
|
// treated as item-scoped too — flat folds must keep emitting
|
|
9420
9767
|
// byte-identical code.
|
|
9421
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;
|
|
9422
9776
|
for (const chainIndex of target.chain) {
|
|
9423
9777
|
const chainStep = actionSteps[chainIndex];
|
|
9424
9778
|
if (!chainStep)
|
|
9425
9779
|
continue;
|
|
9780
|
+
if (chainStep.produces.some((p) => p.kind === "header")) {
|
|
9781
|
+
chainUsesHeaderThreading = true;
|
|
9782
|
+
}
|
|
9426
9783
|
// The zero-variance guard lives inside `parameterizeUrl` itself
|
|
9427
9784
|
// (see above) so it can skip only the threaded-value splice while
|
|
9428
9785
|
// still letting a spec-declared drillParamBindings substitution
|
|
@@ -9444,13 +9801,17 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9444
9801
|
const matchLines = emitFoldMatchAndMergeLines(terminalStep, target, itemVar, suffix, joinAccessor);
|
|
9445
9802
|
if (referencesItemVar) {
|
|
9446
9803
|
itemScopedLines.push(...chainLines, ...matchLines);
|
|
9804
|
+
if (chainLines.length > 0)
|
|
9805
|
+
hasItemScopedFetch = true;
|
|
9806
|
+
if (chainUsesHeaderThreading)
|
|
9807
|
+
hasItemScopedHeaderThreading = true;
|
|
9447
9808
|
}
|
|
9448
9809
|
else {
|
|
9449
9810
|
hoistedChainLines.push(...chainLines);
|
|
9450
9811
|
itemScopedLines.push(...matchLines);
|
|
9451
9812
|
}
|
|
9452
9813
|
}
|
|
9453
|
-
lines.push(...hoistedChainLines, ...itemOpenLines,
|
|
9814
|
+
lines.push(...hoistedChainLines, ...emitItemLoopLines(itemOpenLines, itemScopedLines, itemCloseLines, itemVar, hasItemScopedFetch && !hasItemScopedHeaderThreading), ...ancestorCloseLines, "");
|
|
9454
9815
|
}
|
|
9455
9816
|
return lines;
|
|
9456
9817
|
};
|
|
@@ -9469,13 +9830,19 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
9469
9830
|
const executeHttpBody = multiStepBody
|
|
9470
9831
|
? multiStepBody
|
|
9471
9832
|
: paginationSignal
|
|
9472
|
-
?
|
|
9833
|
+
? buildPaginatedFetchLoopExecuteHttpBody({
|
|
9473
9834
|
pascal,
|
|
9474
|
-
gqlOperationNameExpr,
|
|
9475
|
-
queryConstName: `${pascal.toUpperCase()}_QUERY`,
|
|
9476
9835
|
gqlVariablesExpr,
|
|
9477
9836
|
signal: paginationSignal,
|
|
9478
9837
|
foldMergeLines: paginatedFoldMergeLines,
|
|
9838
|
+
valueConstraints,
|
|
9839
|
+
fetchCall: gql
|
|
9840
|
+
? {
|
|
9841
|
+
kind: "gql",
|
|
9842
|
+
gqlOperationNameExpr,
|
|
9843
|
+
queryConstName: `${pascal.toUpperCase()}_QUERY`,
|
|
9844
|
+
}
|
|
9845
|
+
: { kind: "rest", endpointPath },
|
|
9479
9846
|
})
|
|
9480
9847
|
: gql
|
|
9481
9848
|
? ` const data = await getGql(context.baseUrl)(${gqlOperationNameExpr}, ${pascal.toUpperCase()}_QUERY, ${gqlVariablesExpr});
|
|
@@ -9592,7 +9959,7 @@ export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
|
9592
9959
|
// since the multipart wire format is what makes that field's
|
|
9593
9960
|
// JSON-stringified encoding parseable.
|
|
9594
9961
|
`
|
|
9595
|
-
: ""}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}
|
|
9596
9963
|
},
|
|
9597
9964
|
${executeHttpMethodBlock}
|
|
9598
9965
|
/** Browser fallback: Stagehand + Steel — invoked only when hot path fails. */
|
|
@@ -10132,6 +10499,21 @@ async function resolveFormSchema(specifier) {
|
|
|
10132
10499
|
logger.info(`form-schema: ${specifier === load_form_schema_1.FORM_SCHEMA_NONE ? "none (no ATS form recovery)" : `custom keys from ${specifier}`}`);
|
|
10133
10500
|
return formSchema;
|
|
10134
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
|
+
}
|
|
10135
10517
|
/**
|
|
10136
10518
|
* The capture whose own response `assertRequiredUrlFieldsReferenced`'s
|
|
10137
10519
|
* scan actually describes: a submission flow returns
|
|
@@ -10296,6 +10678,7 @@ async function main() {
|
|
|
10296
10678
|
let emit = "ts";
|
|
10297
10679
|
let vocabularySpecifier = "";
|
|
10298
10680
|
let formSchemaSpecifier = "";
|
|
10681
|
+
let valueConstraintsSpecifier = "";
|
|
10299
10682
|
let runDir;
|
|
10300
10683
|
let allowEmptyCapture = false;
|
|
10301
10684
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -10305,6 +10688,8 @@ async function main() {
|
|
|
10305
10688
|
vocabularySpecifier = args[++i];
|
|
10306
10689
|
else if (args[i] === "--form-schema" && args[i + 1])
|
|
10307
10690
|
formSchemaSpecifier = args[++i];
|
|
10691
|
+
else if (args[i] === "--value-constraints" && args[i + 1])
|
|
10692
|
+
valueConstraintsSpecifier = args[++i];
|
|
10308
10693
|
else if (args[i] === "--run-dir" && args[i + 1])
|
|
10309
10694
|
runDir = args[++i];
|
|
10310
10695
|
else if (args[i] === "--force")
|
|
@@ -10374,7 +10759,7 @@ async function main() {
|
|
|
10374
10759
|
return [];
|
|
10375
10760
|
}
|
|
10376
10761
|
})();
|
|
10377
|
-
const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, } = (() => {
|
|
10762
|
+
const { flowSteps, frameSelector, submitEndpointPattern, submitBodyPattern, requireSubmitEndpointMatch, displayName, foldReturnSpec, fallbackGateSpec, } = (() => {
|
|
10378
10763
|
const flowFileContents = (() => {
|
|
10379
10764
|
try {
|
|
10380
10765
|
return (0, node_fs_1.readFileSync)(flowFile, "utf8");
|
|
@@ -10387,6 +10772,10 @@ async function main() {
|
|
|
10387
10772
|
// valid `foldReturn` still resolves when the rest of the flow file is
|
|
10388
10773
|
// degenerate — the two declarations fail independently.
|
|
10389
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);
|
|
10390
10779
|
try {
|
|
10391
10780
|
const raw = flowFileContents === null ? null : JSON.parse(flowFileContents);
|
|
10392
10781
|
if (Array.isArray(raw))
|
|
@@ -10398,6 +10787,7 @@ async function main() {
|
|
|
10398
10787
|
requireSubmitEndpointMatch: false,
|
|
10399
10788
|
displayName: undefined,
|
|
10400
10789
|
foldReturnSpec,
|
|
10790
|
+
fallbackGateSpec,
|
|
10401
10791
|
};
|
|
10402
10792
|
if (raw !== null &&
|
|
10403
10793
|
typeof raw === "object" &&
|
|
@@ -10412,6 +10802,7 @@ async function main() {
|
|
|
10412
10802
|
requireSubmitEndpointMatch: obj.requireSubmitEndpointMatch ?? false,
|
|
10413
10803
|
displayName: obj.displayName,
|
|
10414
10804
|
foldReturnSpec,
|
|
10805
|
+
fallbackGateSpec,
|
|
10415
10806
|
};
|
|
10416
10807
|
}
|
|
10417
10808
|
return {
|
|
@@ -10422,6 +10813,7 @@ async function main() {
|
|
|
10422
10813
|
requireSubmitEndpointMatch: false,
|
|
10423
10814
|
displayName: undefined,
|
|
10424
10815
|
foldReturnSpec,
|
|
10816
|
+
fallbackGateSpec,
|
|
10425
10817
|
};
|
|
10426
10818
|
}
|
|
10427
10819
|
catch {
|
|
@@ -10433,6 +10825,7 @@ async function main() {
|
|
|
10433
10825
|
requireSubmitEndpointMatch: false,
|
|
10434
10826
|
displayName: undefined,
|
|
10435
10827
|
foldReturnSpec,
|
|
10828
|
+
fallbackGateSpec,
|
|
10436
10829
|
};
|
|
10437
10830
|
}
|
|
10438
10831
|
})();
|
|
@@ -10450,6 +10843,10 @@ async function main() {
|
|
|
10450
10843
|
// Consumer-supplied wire keys for ATS form-schema recovery, or null. When
|
|
10451
10844
|
// null the recovery functions no-op — the engine hardcodes no vendor format.
|
|
10452
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);
|
|
10453
10850
|
const pascal = toPascalCase(siteId);
|
|
10454
10851
|
// Read the flow's declared own-backend hosts BEFORE deriving baseUrl, so
|
|
10455
10852
|
// deriveBaseUrl itself can gate on them: a third-party host that happens
|
|
@@ -10871,7 +11268,12 @@ async function main() {
|
|
|
10871
11268
|
parsedOperationName(primaryGraphQLOperation.capture.query ?? ""))
|
|
10872
11269
|
: (fallbackGraphQLCapture?.operationName ??
|
|
10873
11270
|
parsedOperationName(fallbackGraphQLCapture?.query ?? "")),
|
|
10874
|
-
|
|
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),
|
|
10875
11277
|
allCaptures: activeCaptures,
|
|
10876
11278
|
auxFiles,
|
|
10877
11279
|
multiStepBody,
|
|
@@ -10887,10 +11289,12 @@ async function main() {
|
|
|
10887
11289
|
discoveredRawOptionFields,
|
|
10888
11290
|
discoveredAdditionalBodyKeys,
|
|
10889
11291
|
discoveredStructuredKeys,
|
|
11292
|
+
valueConstraints,
|
|
10890
11293
|
payloadFieldNames: browserFlow.payloadFieldNames,
|
|
10891
11294
|
optionalPayloadFieldNames: browserFlow.optionalPayloadFieldNames,
|
|
10892
11295
|
headerBindings,
|
|
10893
11296
|
unpopulatedDeclaredVariables: primaryGraphQLOperation?.unpopulatedDeclaredVariables ?? [],
|
|
11297
|
+
fallbackGateSpec,
|
|
10894
11298
|
};
|
|
10895
11299
|
const contractCode = emitContractTs(contractOpts);
|
|
10896
11300
|
return {
|