@enricai/barnacle 1.12.69 → 1.12.70
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/scripts/recon-browser.js +1 -1
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +6 -1
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +107 -43
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -1142,8 +1142,16 @@ function deriveRequestHeaders(captures, replays, baseUrl, submitPatterns = null,
|
|
|
1142
1142
|
}
|
|
1143
1143
|
return baseline;
|
|
1144
1144
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1145
|
+
/**
|
|
1146
|
+
* Classifies the flow as GraphQL only from own-backend-host traffic, so a
|
|
1147
|
+
* third-party host (chat widget, analytics SDK) making real GraphQL calls
|
|
1148
|
+
* never flips a plain REST/JSON backend's classification.
|
|
1149
|
+
*/
|
|
1150
|
+
function isGraphQL(captures, ownBackendHostnames = [], fallbackDomain = null) {
|
|
1151
|
+
const hasHostProvenance = ownBackendHostnames.length > 0 || fallbackDomain !== null;
|
|
1152
|
+
return captures.some((c) => (!hasHostProvenance ||
|
|
1153
|
+
(0, capture_filters_1.isAllowedFixtureHost)(captureHostname(c.url), ownBackendHostnames, fallbackDomain)) &&
|
|
1154
|
+
(c.operationName !== null || (0, capture_filters_1.parsedOperationName)(c.query ?? "") !== null));
|
|
1147
1155
|
}
|
|
1148
1156
|
/**
|
|
1149
1157
|
* The single own-backend capture the raw GraphQL fallback (no
|
|
@@ -1225,9 +1233,12 @@ function operationGroupKey(capture) {
|
|
|
1225
1233
|
* never independently resolve a different primary.
|
|
1226
1234
|
*/
|
|
1227
1235
|
function computeEmittedPrimaryAnchor(gql, endpointPath, gqlOperationName, gqlQuery) {
|
|
1228
|
-
|
|
1236
|
+
// Mirrors emitContractTs's own isGqlEmission gate: `gql` alone can be true
|
|
1237
|
+
// with a null gqlQuery (see gqlQuery's `?? null` fallback), in which case
|
|
1238
|
+
// emitContractTs falls through to REST emission — this anchor must agree.
|
|
1239
|
+
if (!gql || gqlQuery === null)
|
|
1229
1240
|
return null;
|
|
1230
|
-
const name = gqlOperationName ?? (0, capture_filters_1.parsedOperationName)(gqlQuery
|
|
1241
|
+
const name = gqlOperationName ?? (0, capture_filters_1.parsedOperationName)(gqlQuery) ?? "anonymous";
|
|
1231
1242
|
return `${endpointPath}::${name}`;
|
|
1232
1243
|
}
|
|
1233
1244
|
/**
|
|
@@ -1584,8 +1595,26 @@ function resolveManifestActionSequence(runRoot, captures) {
|
|
|
1584
1595
|
* Exported for tests: this predicate decides what a generated plugin will POST
|
|
1585
1596
|
* at a live site, and it is the only gate between a browser's incidental
|
|
1586
1597
|
* chatter and the emitted hot path.
|
|
1587
|
-
|
|
1588
|
-
|
|
1598
|
+
*
|
|
1599
|
+
* `applyFinalAnchorNarrowing` lets a caller that will itself truncate the
|
|
1600
|
+
* result via {@link truncateActionSequenceAtSubmitPattern} opt out of the
|
|
1601
|
+
* final structural-relevance anchor pass, which can drop a genuine earlier
|
|
1602
|
+
* chain step that shares no token with the terminal submit endpoint.
|
|
1603
|
+
*/
|
|
1604
|
+
function extractActionSequence(captures, submitPatterns = null, foldReturnSpec = null, ownBackendHostnames = [], fallbackDomain = null,
|
|
1605
|
+
// The final structural-relevance anchor pass below (guarded on
|
|
1606
|
+
// hasSubmitEndpointAnchor) is tuned for isolating a submission from
|
|
1607
|
+
// same-URL incidental chrome, not for pruning a genuine multi-step chain:
|
|
1608
|
+
// an earlier plain single-word step (e.g. `/lodging/hold/`) that shares no
|
|
1609
|
+
// compound token with the terminal submit endpoint reads as "unrelated" to
|
|
1610
|
+
// it and gets dropped, even though it's a real prior step whose body the
|
|
1611
|
+
// caller still needs. truncateActionSequenceAtSubmitPattern's own
|
|
1612
|
+
// index-slice already does the right, less-aggressive narrowing (keep
|
|
1613
|
+
// everything up to and including the last submit/fold match) — so a caller
|
|
1614
|
+
// that immediately truncates this result opts out with `false` to keep
|
|
1615
|
+
// earlier chain steps intact while still getting the structural-isolation
|
|
1616
|
+
// exemption above.
|
|
1617
|
+
applyFinalAnchorNarrowing = true) {
|
|
1589
1618
|
const matchesFoldReturn = compileFoldReturnEndpointMatcher(foldReturnSpec);
|
|
1590
1619
|
// Callers with no host-provenance data (the exported function's unit
|
|
1591
1620
|
// tests) pass neither ownBackendHostnames nor fallbackDomain — in that
|
|
@@ -1607,6 +1636,14 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
|
|
|
1607
1636
|
// the submission from same-URL chrome" behavior).
|
|
1608
1637
|
const hasSubmitEndpointAnchor = hasHostProvenance && submitPatterns !== null && submitPatterns.endpoint !== null;
|
|
1609
1638
|
const matchesSubmit = compileSubmitMatcher(hasSubmitEndpointAnchor ? { endpoint: null, body: submitPatterns.body } : submitPatterns);
|
|
1639
|
+
// Compiled once, ahead of structural isolation below, so a capture matching
|
|
1640
|
+
// the flow's own declared submitEndpointPattern can be exempted there the
|
|
1641
|
+
// same way a declared foldReturn endpointPattern already is — both are
|
|
1642
|
+
// author-declared, both are authoritative by construction, so the isolation
|
|
1643
|
+
// pass (which exists to catch INCIDENTAL same-host noise the flow never
|
|
1644
|
+
// named) must not treat one differently from the other.
|
|
1645
|
+
const declaredSubmitEndpointRx = submitPatterns?.endpoint != null ? new RegExp(submitPatterns.endpoint) : null;
|
|
1646
|
+
const matchesDeclaredSubmitEndpoint = (capture) => declaredSubmitEndpointRx?.test(capture.url) ?? false;
|
|
1610
1647
|
const hostGated = captures
|
|
1611
1648
|
.map((capture, index) => ({ capture, index }))
|
|
1612
1649
|
.filter(({ capture }) => {
|
|
@@ -1630,9 +1667,14 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
|
|
|
1630
1667
|
// (e.g. a marketing/promotions endpoint fired incidentally by a page load,
|
|
1631
1668
|
// admitted above because it too is a 2xx own-backend POST). Drop a
|
|
1632
1669
|
// structurally-isolated member of the pool first — a compound-path capture
|
|
1633
|
-
// (e.g. `/site-banner`) sharing no token with anything else host-gated in
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1670
|
+
// (e.g. `/site-banner`) sharing no token with anything else host-gated in
|
|
1671
|
+
// — unless it matches the flow's own declared submitEndpointPattern (see
|
|
1672
|
+
// the exemption below): a densely-tokenized real submission path (e.g.
|
|
1673
|
+
// `available-sailings`) can look exactly as isolated as incidental noise
|
|
1674
|
+
// by path shape alone, and dropping it here would strip it out before the
|
|
1675
|
+
// structural-relevance anchor pass further down ever gets to treat it as
|
|
1676
|
+
// authoritative. Guarded to pools of 3+: a real chain's own steps are
|
|
1677
|
+
// usually plain single-word
|
|
1636
1678
|
// paths with no tokens of their own (never flagged, see
|
|
1637
1679
|
// isStructurallyIsolatedCapture), so this only ever removes a capture that
|
|
1638
1680
|
// is BOTH compound-path AND unrelated to every other admitted capture —
|
|
@@ -1663,11 +1705,14 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
|
|
|
1663
1705
|
.filter((h, j) => denselyNameSpaced ? safeUrlPathname(h.capture.url) !== path : j !== i)
|
|
1664
1706
|
.map((h) => safeUrlPathname(h.capture.url));
|
|
1665
1707
|
// A capture admitted via the flow's own declared foldReturnSpec
|
|
1666
|
-
// endpointPattern (the drill-down target)
|
|
1667
|
-
//
|
|
1708
|
+
// endpointPattern (the drill-down target), or matching the flow's
|
|
1709
|
+
// own declared submitEndpointPattern, is authoritative by
|
|
1710
|
+
// construction — the flow author named it explicitly — so both are
|
|
1668
1711
|
// exempt from token-overlap isolation, which exists to catch
|
|
1669
1712
|
// INCIDENTAL same-host captures the flow never declared.
|
|
1670
|
-
return matchesFoldReturn(capture) ||
|
|
1713
|
+
return (matchesFoldReturn(capture) ||
|
|
1714
|
+
matchesDeclaredSubmitEndpoint(capture) ||
|
|
1715
|
+
!(0, capture_filters_1.isStructurallyIsolatedCapture)(path, otherPaths));
|
|
1671
1716
|
})
|
|
1672
1717
|
: hostGated;
|
|
1673
1718
|
// Narrow further using structural relevance, anchored on whichever
|
|
@@ -1677,15 +1722,14 @@ function extractActionSequence(captures, submitPatterns = null, foldReturnSpec =
|
|
|
1677
1722
|
// authoritative reference, so this pass is a no-op — mirroring
|
|
1678
1723
|
// compileSubmitMatcher's own null-pattern passthrough — rather than
|
|
1679
1724
|
// guessing at a reference set.
|
|
1680
|
-
if (!hasSubmitEndpointAnchor)
|
|
1725
|
+
if (!hasSubmitEndpointAnchor || !applyFinalAnchorNarrowing)
|
|
1681
1726
|
return structurallyGated;
|
|
1682
|
-
const endpointRx = new RegExp(submitPatterns.endpoint);
|
|
1683
1727
|
const referencePaths = structurallyGated
|
|
1684
|
-
.filter(({ capture }) =>
|
|
1728
|
+
.filter(({ capture }) => matchesDeclaredSubmitEndpoint(capture))
|
|
1685
1729
|
.map(({ capture }) => safeUrlPathname(capture.url));
|
|
1686
1730
|
if (referencePaths.length === 0)
|
|
1687
1731
|
return structurallyGated;
|
|
1688
|
-
return structurallyGated.filter(({ capture }) =>
|
|
1732
|
+
return structurallyGated.filter(({ capture }) => matchesDeclaredSubmitEndpoint(capture) ||
|
|
1689
1733
|
(0, capture_filters_1.isStructurallyRelevantCapture)(safeUrlPathname(capture.url), referencePaths));
|
|
1690
1734
|
}
|
|
1691
1735
|
/**
|
|
@@ -9005,31 +9049,30 @@ primaryIdentityAnchor = null) {
|
|
|
9005
9049
|
specPlan,
|
|
9006
9050
|
];
|
|
9007
9051
|
}
|
|
9008
|
-
// Keyed by the (primaryStepIndex, primaryArrayPath) pair, not
|
|
9009
|
-
//
|
|
9010
|
-
//
|
|
9011
|
-
//
|
|
9012
|
-
//
|
|
9013
|
-
//
|
|
9014
|
-
//
|
|
9015
|
-
//
|
|
9016
|
-
//
|
|
9017
|
-
//
|
|
9018
|
-
//
|
|
9019
|
-
//
|
|
9020
|
-
//
|
|
9021
|
-
|
|
9052
|
+
// Keyed by the (primaryStepIndex, primaryArrayPath) pair, not raw step
|
|
9053
|
+
// indices: that pair is the only thing that identifies whether the spec
|
|
9054
|
+
// describes a primary a structural plan ALREADY owns. A structural
|
|
9055
|
+
// target's own `drillStepIndex` (or any index in its replay chain)
|
|
9056
|
+
// coinciding with a step the spec's own resolution happens to touch is
|
|
9057
|
+
// not, on its own, evidence of a real conflict — an unrelated,
|
|
9058
|
+
// independent primary/drill pair can legitimately enter through (or
|
|
9059
|
+
// replay) the exact same underlying capture the spec's chain does (e.g.
|
|
9060
|
+
// a shared session/auth bootstrap call reused by two unrelated
|
|
9061
|
+
// features), and both targets replaying it per their own items is
|
|
9062
|
+
// exactly what {@link resolveSpecMatchedPrimaryItemIndexAlongChain}'s
|
|
9063
|
+
// multi-hop resolution expects — see the shared-bootstrap-hop-noise
|
|
9064
|
+
// regression test. Excluding on raw index overlap (as an earlier version
|
|
9065
|
+
// of this check did, for BOTH the spec's `drillStepIndex` and its whole
|
|
9066
|
+
// `chain`) silently dropped a real, resolvable declared join whenever
|
|
9067
|
+
// some unrelated noisy structural plan merely touched the same index by
|
|
9068
|
+
// coincidence, which is exactly the "declared fold rejected" failure
|
|
9069
|
+
// mode this function exists to prevent.
|
|
9022
9070
|
const consumedPrimarySteps = new Set();
|
|
9023
9071
|
for (const plan of structuralPlansWithPerOccurrenceOverrides) {
|
|
9024
9072
|
const planPrimaryEndpointKey = endpointKey(actions[plan.primaryStepIndex].capture.url);
|
|
9025
9073
|
consumedPrimarySteps.add(`${planPrimaryEndpointKey}:${JSON.stringify(plan.primaryArrayPath)}`);
|
|
9026
|
-
for (const target of plan.targets) {
|
|
9027
|
-
consumedIndices.add(target.drillStepIndex);
|
|
9028
|
-
}
|
|
9029
9074
|
}
|
|
9030
|
-
const specConsumesOnlyItsOwnIndices = !
|
|
9031
|
-
!consumedPrimarySteps.has(`${specPrimaryEndpointKey}:${JSON.stringify(specPlan.primaryArrayPath)}`) &&
|
|
9032
|
-
specPlan.targets.every((target) => target.chain.every((chainIndex) => !consumedIndices.has(chainIndex)));
|
|
9075
|
+
const specConsumesOnlyItsOwnIndices = !consumedPrimarySteps.has(`${specPrimaryEndpointKey}:${JSON.stringify(specPlan.primaryArrayPath)}`);
|
|
9033
9076
|
if (specConsumesOnlyItsOwnIndices)
|
|
9034
9077
|
return [...structuralPlansWithPerOccurrenceOverrides, specPlan];
|
|
9035
9078
|
// The spec's primary endpoint/array is already consumed by a structural
|
|
@@ -9677,6 +9720,11 @@ function buildContractChecklist(opts) {
|
|
|
9677
9720
|
function emitContractTs(opts) {
|
|
9678
9721
|
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;
|
|
9679
9722
|
const { browserFallbackGate, httpTimeoutMs } = fallbackGateSpec;
|
|
9723
|
+
/** Single source of truth for every GraphQL-emission decision below
|
|
9724
|
+
* (clientImport, gqlCacheBlock, queryConst, and both executeHttpBody
|
|
9725
|
+
* fetch-call sites) — `gql` alone can be true with a null `gqlQuery`,
|
|
9726
|
+
* which would emit a `${PASCAL}_QUERY` reference with no declaration. */
|
|
9727
|
+
const isGqlEmission = gql && gqlQuery !== null;
|
|
9680
9728
|
/** Rendered `meta.browserFallbackGate` literal: `false` verbatim, a
|
|
9681
9729
|
* list-derived arrow-function predicate matching against `error.name`, or
|
|
9682
9730
|
* "" (omitted) when the flow declared no gate — preserving byte-identical
|
|
@@ -10134,15 +10182,15 @@ function emitContractTs(opts) {
|
|
|
10134
10182
|
// `noUnusedVariables`.
|
|
10135
10183
|
const clientImport = omitExecuteHttp
|
|
10136
10184
|
? ""
|
|
10137
|
-
:
|
|
10185
|
+
: isGqlEmission
|
|
10138
10186
|
? `import { createGraphqlClient } from "${ENGINE_PKG}/scraper/graphql-client";${needsFoldHttpClient ? `\nimport { createHttpClient } from "${ENGINE_PKG}/scraper/http-client";` : ""}`
|
|
10139
10187
|
: `import { createHttpClient } from "${ENGINE_PKG}/scraper/http-client";`;
|
|
10140
|
-
const queryConst = !omitExecuteHttp &&
|
|
10188
|
+
const queryConst = !omitExecuteHttp && isGqlEmission
|
|
10141
10189
|
? `\n// Lifted verbatim from recon capture. The adjacent response schema is drift-tolerant by construction (dropped __typename, .loose() objects), so this query text is not hand-trimmed.\nconst ${pascal.toUpperCase()}_QUERY = \`${gqlQuery.trim()}\`;\n`
|
|
10142
10190
|
: "";
|
|
10143
10191
|
const gqlCacheBlock = omitExecuteHttp
|
|
10144
10192
|
? ""
|
|
10145
|
-
:
|
|
10193
|
+
: isGqlEmission
|
|
10146
10194
|
? `
|
|
10147
10195
|
type GqlFn = (operationName: string, query: string, variables: Record<string, unknown>) => Promise<${pascal}Response>;
|
|
10148
10196
|
|
|
@@ -10491,7 +10539,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
10491
10539
|
signal: paginationSignal,
|
|
10492
10540
|
foldMergeLines: paginatedFoldMergeLines,
|
|
10493
10541
|
valueConstraints,
|
|
10494
|
-
fetchCall:
|
|
10542
|
+
fetchCall: isGqlEmission
|
|
10495
10543
|
? {
|
|
10496
10544
|
kind: "gql",
|
|
10497
10545
|
gqlOperationNameExpr,
|
|
@@ -10499,7 +10547,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
10499
10547
|
}
|
|
10500
10548
|
: { kind: "rest", endpointPath },
|
|
10501
10549
|
})
|
|
10502
|
-
:
|
|
10550
|
+
: isGqlEmission
|
|
10503
10551
|
? ` const data = await getGql(context.baseUrl)(${gqlOperationNameExpr}, ${pascal.toUpperCase()}_QUERY, ${gqlVariablesExpr});
|
|
10504
10552
|
${dataFoldMergeBlock} return { data };`
|
|
10505
10553
|
: ` const data = await httpClient(\`\${context.baseUrl}${endpointPath}\`, {
|
|
@@ -11591,7 +11639,7 @@ async function main() {
|
|
|
11591
11639
|
const minTime = deriveMinTime(rateLimits);
|
|
11592
11640
|
const hasRateLimitProbeData = rateLimits.some((f) => f.safeRps !== null);
|
|
11593
11641
|
const safeRps = rateLimits.find((f) => f.safeRps !== null)?.safeRps ?? Math.floor(1000 / minTime);
|
|
11594
|
-
const gql = isGraphQL(activeCaptures);
|
|
11642
|
+
const gql = isGraphQL(activeCaptures, ownBackendHostnames, fallbackDomain);
|
|
11595
11643
|
// Hoisted so both the primary-operation gate below and rawActionCaptures
|
|
11596
11644
|
// (further down) read the same computed sequence instead of calling the
|
|
11597
11645
|
// extractor twice. Computed unfiltered (submitPatterns: null) — a
|
|
@@ -11669,9 +11717,25 @@ async function main() {
|
|
|
11669
11717
|
// keeps that whole chain; the gap between this and the unfiltered
|
|
11670
11718
|
// sequence is logged below for visibility, but the declared pattern is
|
|
11671
11719
|
// never overridden by the richer unfiltered sequence.
|
|
11720
|
+
//
|
|
11721
|
+
// Truncated against a pool RE-EXTRACTED with the declared pattern passed
|
|
11722
|
+
// through (not against unfilteredHeuristicActionCaptures, which is built
|
|
11723
|
+
// with submitPatterns forced to null purely to get an unbiased baseline
|
|
11724
|
+
// for the disagreement check below). extractActionSequence's own
|
|
11725
|
+
// noise/structural-isolation passes have no visibility into the declared
|
|
11726
|
+
// pattern when it's withheld, so on an archive noisy enough to make a
|
|
11727
|
+
// genuine submission capture look structurally isolated, that capture
|
|
11728
|
+
// can be dropped before truncation ever sees it — a real match then
|
|
11729
|
+
// reads as "0 captures" even though the archive contains plenty. Passing
|
|
11730
|
+
// submitPatterns through lets extractActionSequence's own submit-anchor
|
|
11731
|
+
// narrowing (REST) / per-capture submit match (GraphQL) keep the
|
|
11732
|
+
// genuine captures amid that noise, so truncation has them to find.
|
|
11733
|
+
const patternedExtractedActionCaptures = gql
|
|
11734
|
+
? dedupRedundantSameOperationCaptures(extractGraphQLActionSequence(activeCaptures, submitPatterns, foldReturnSpec, ownBackendHostnames, fallbackDomain), primaryGraphQLOperation)
|
|
11735
|
+
: collapseRedundantSameEndpointCaptures(collapseRedundantPatches(extractActionSequence(activeCaptures, submitPatterns, foldReturnSpec, ownBackendHostnames, fallbackDomain, false)));
|
|
11672
11736
|
const patternedHeuristicActionCaptures = submitPatterns.endpoint === null && submitPatterns.body === null
|
|
11673
11737
|
? unfilteredHeuristicActionCaptures
|
|
11674
|
-
: truncateActionSequenceAtSubmitPattern(
|
|
11738
|
+
: truncateActionSequenceAtSubmitPattern(patternedExtractedActionCaptures, submitPatterns, foldReturnSpec);
|
|
11675
11739
|
const patternUndercounts = patternedHeuristicActionCaptures.length < unfilteredHeuristicActionCaptures.length;
|
|
11676
11740
|
if (patternUndercounts && requireSubmitEndpointMatch) {
|
|
11677
11741
|
// Distinct wording from the non-required case below: this pattern is never
|