@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.
@@ -1142,8 +1142,16 @@ function deriveRequestHeaders(captures, replays, baseUrl, submitPatterns = null,
1142
1142
  }
1143
1143
  return baseline;
1144
1144
  }
1145
- function isGraphQL(captures) {
1146
- return captures.some((c) => c.operationName !== null || (0, capture_filters_1.parsedOperationName)(c.query ?? "") !== null);
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
- if (!gql)
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 ?? "") ?? "anonymous";
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
- function extractActionSequence(captures, submitPatterns = null, foldReturnSpec = null, ownBackendHostnames = [], fallbackDomain = null) {
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
- // regardless of whether a submitEndpointPattern is declared. Guarded to
1635
- // pools of 3+: a real chain's own steps are usually plain single-word
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) is authoritative by
1667
- // construction — the flow author named it explicitly — so it is
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) || !(0, capture_filters_1.isStructurallyIsolatedCapture)(path, otherPaths);
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 }) => endpointRx.test(capture.url))
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 }) => endpointRx.test(capture.url) ||
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
- // primaryStepIndex alone — a structural plan only ever consumed ITS OWN
9010
- // array on that step, not the whole step. A spec whose resultsPath names
9011
- // a second, structurally-undetected array on that exact same primary step
9012
- // has already been proven independent by the samePrimaryPlan lookup above
9013
- // (its primaryArrayPath differs from every structural plan's), so keying
9014
- // solely on the step index would wrongly treat it as already consumed and
9015
- // silently drop it. consumedIndices tracks only the steps a plan folds
9016
- // FROM (each target's drillStepIndex), not its whole replay chain — a
9017
- // target's chain threads through steps it depends on to reach per-item
9018
- // data, but only its drillStepIndex is the call it actually folds from,
9019
- // so sweeping the whole chain would falsely claim steps the spec never
9020
- // contends for.
9021
- const consumedIndices = new Set();
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 = !consumedIndices.has(specPlan.primaryStepIndex) &&
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
- : gql
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 && gql && gqlQuery
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
- : gql
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: gql
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
- : gql
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(unfilteredHeuristicActionCaptures, submitPatterns, foldReturnSpec);
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