@enricai/barnacle 1.12.64 → 1.12.66

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.
@@ -4493,7 +4493,15 @@ function replaceGuardedAgainstExistingPlaceholders(text, pattern, bindingByValue
4493
4493
  * first, and the literal is gone by the time the KV pass runs — masking
4494
4494
  * payload precedence on every call after the one that scraped it.
4495
4495
  */
4496
- function interpolateStateValues(template, priorSteps, targetCapture, payloadAccessorByValue = new Map(), isJsonBody = false, producerBoundaryBindings = new Map(), stepIndex = -1, topLevelPayloadKvValues = new Set()) {
4496
+ function interpolateStateValues(template, priorSteps, targetCapture, payloadAccessorByValue = new Map(), isJsonBody = false, producerBoundaryBindings = new Map(), stepIndex = -1, topLevelPayloadKvValues = new Set(),
4497
+ // A fold target's own join-field value (see `joinFieldValuesByStep` in
4498
+ // `emitMultiStepExecuteHttp`) must stay a literal here — binding it to a
4499
+ // prior step's produced state var (e.g. `${productId}`, always the FIRST
4500
+ // primary item's value) freezes every per-item drill call onto that one
4501
+ // item; the fold loop's own later `parameterize` pass is what rebinds the
4502
+ // literal to `${item.<field>}` per iteration, but only if the literal
4503
+ // text survives this pass untouched.
4504
+ excludedJoinFieldValues = new Set()) {
4497
4505
  const stateBindings = deriveStateVarByValue(priorSteps, targetCapture);
4498
4506
  const bindingByValue = new Map();
4499
4507
  for (const [value, accessor] of payloadAccessorByValue) {
@@ -4508,6 +4516,8 @@ function interpolateStateValues(template, priorSteps, targetCapture, payloadAcce
4508
4516
  continue;
4509
4517
  if (topLevelPayloadKvValues.has(value) && !isProducerStep)
4510
4518
  continue;
4519
+ if (excludedJoinFieldValues.has(value) && !isProducerStep)
4520
+ continue;
4511
4521
  bindingByValue.set(value, `\${${binding.varName}}`);
4512
4522
  sourceNameByValue.set(value, binding.sourceName);
4513
4523
  if (binding.restricted)
@@ -5525,7 +5535,7 @@ pascalName = null) {
5525
5535
  // length/chain-eligibility scoping doesn't happen to catch the coincidence.
5526
5536
  const url = (0, capture_filters_1.isZeroVarianceRepeatCapture)(cap, actions.map((a) => a.capture))
5527
5537
  ? cap.url
5528
- : interpolateStateValues(cap.url, prior, cap, payloadAccessorByValue, false, producerBoundaryBindings, i);
5538
+ : interpolateStateValues(cap.url, prior, cap, payloadAccessorByValue, false, producerBoundaryBindings, i, new Set(), joinFieldValuesByStep.get(i) ?? new Set());
5529
5539
  // Form-schema substitution runs first on the raw recon body so its
5530
5540
  // field-id-anchored matches see the original JSON. State-threading and
5531
5541
  // payload key-value passes then run on top. Option-id substitution runs
@@ -5595,7 +5605,7 @@ pascalName = null) {
5595
5605
  ? applyUrlParamPayloadSubstitutions(rawBodyWithProducerBoundary, parsedBody, urlParamBindings)
5596
5606
  : rawBodyWithProducerBoundary;
5597
5607
  const bodyAfterStateAndKv = rawBodyWithUrlParams
5598
- ? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies)
5608
+ ? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithUrlParams, prior, cap, payloadAccessorByValue, true, producerBoundaryBindings, i, topLevelPayloadKvValues, joinFieldValuesByStep.get(i) ?? new Set()), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys, allResponseBodies)
5599
5609
  : "";
5600
5610
  // Mechanism A — generic (plain-JSON, wire-key-anchored) dropdown label→code
5601
5611
  // rewrite. Runs AFTER interpolateStateValues + the payload-KV pass, not
@@ -7885,6 +7895,60 @@ function readValueAtPath(body, path) {
7885
7895
  return node[segment];
7886
7896
  }, body);
7887
7897
  }
7898
+ /**
7899
+ * Turns a flow author's plain dot-separated {@link FoldReturnSpec.resultsPath}
7900
+ * / `drillResultsPath` into the SAME {@link ARRAY_WILDCARD_SEGMENT}-bearing
7901
+ * shape {@link findAllObjectArrayFields} would have discovered structurally,
7902
+ * by walking `rawSegments` against a REAL `body` and inserting the sentinel
7903
+ * wherever the path crosses an array the author's segment doesn't literally
7904
+ * index into. A JSON dot-path is ordinary object-property notation; it has
7905
+ * no convention of its own for "every element of this array" (that spelling
7906
+ * is this module's internal detail), so an author naming a genuinely nested
7907
+ * per-ancestor array — e.g. `"trips.legs"` for a response shaped
7908
+ * `{ trips: [{ legs: [...] }] }` — always omits it. Left unresolved, that
7909
+ * mismatch is silent and total: `objectItemsAtPath`'s plain (no-wildcard)
7910
+ * branch reads a property straight off the array object, resolves to
7911
+ * `undefined`, and the whole spec fails to match ANY primary — and even
7912
+ * where a structural target already exists to fall back onto, every
7913
+ * primaryArrayPath equality check the fold-join merge (
7914
+ * {@link mergeSpecPlanOntoSamePrimary}) runs is a bare `JSON.stringify`
7915
+ * comparison against the structural path's own always-wildcarded form, so a
7916
+ * declared path that's semantically identical but spelled without the
7917
+ * sentinel can never match it either — the structural guess silently wins
7918
+ * regardless of how many per-target override branches are added. Normalizing
7919
+ * at the one point every one of those comparisons ultimately reads from
7920
+ * fixes them all at once, rather than teaching each comparison site its own
7921
+ * tolerance.
7922
+ *
7923
+ * A literal numeric segment (see {@link ARRAY_INDEX_KEY_PATTERN}) is
7924
+ * left as-is — an author who names one exact array element that way already
7925
+ * resolves correctly via `readValueAtPath`'s ordinary bracket access, and
7926
+ * rewriting it to the wildcard would silently widen "this one element" into
7927
+ * "every element". Uses the array's own first object-array item as the
7928
+ * representative to keep walking deeper (mirroring
7929
+ * {@link findAllObjectArrayFieldsUncached}'s own DFS-first-match precedent);
7930
+ * a path that turns out to disagree across sibling elements is exactly what
7931
+ * {@link objectItemsAtPath}'s later wildcard-driven `.flatMap` — not this
7932
+ * one-shot shape walk — is responsible for reconciling.
7933
+ */
7934
+ function resolveDeclaredArrayPath(rawSegments, body) {
7935
+ const resolved = [];
7936
+ let node = body;
7937
+ for (const segment of rawSegments) {
7938
+ if (Array.isArray(node) &&
7939
+ segment !== ARRAY_WILDCARD_SEGMENT &&
7940
+ !ARRAY_INDEX_KEY_PATTERN.test(segment)) {
7941
+ resolved.push(ARRAY_WILDCARD_SEGMENT);
7942
+ node = node.find(isObjectArrayItem);
7943
+ }
7944
+ resolved.push(segment);
7945
+ node =
7946
+ node !== null && typeof node === "object"
7947
+ ? node[segment]
7948
+ : undefined;
7949
+ }
7950
+ return resolved;
7951
+ }
7888
7952
  /** The object items of the array at `path` — the same subset
7889
7953
  * {@link findObjectArrayField} exposes as `items`, but anchored to a
7890
7954
  * caller-supplied path instead of discovered by DFS. `null` when `path`
@@ -8184,14 +8248,33 @@ function compileFoldReturnEndpointMatcher(spec) {
8184
8248
  function compileFoldReturnResultsMatcher(spec) {
8185
8249
  if (spec === null)
8186
8250
  return () => false;
8187
- const resultsPath = spec.resultsPath.split(".");
8188
- return (capture) => objectItemsAtPath(capture.responseBody, resultsPath) !== null;
8251
+ const rawResultsPath = spec.resultsPath.split(".");
8252
+ return (capture) => objectItemsAtPath(capture.responseBody, resolveDeclaredArrayPath(rawResultsPath, capture.responseBody)) !== null;
8189
8253
  }
8190
8254
  function buildFoldPlanFromSpec(actions, spec,
8191
8255
  // See detectDrillDownFoldPlan's identical parameter — constrains this
8192
8256
  // function's own primaryStepIndex loop to the same anchor identity.
8193
- primaryIdentityAnchor = null) {
8194
- const primaryArrayPath = spec.resultsPath.split(".");
8257
+ primaryIdentityAnchor = null,
8258
+ // When given, restricts the loop to this ONE primaryStepIndex instead of
8259
+ // scanning every occurrence and keeping only the freshest. Used by
8260
+ // mergeSpecPlanOntoSamePrimary to resolve a declared joinFields override
8261
+ // independently for EACH re-issued/paginated primary occurrence, since the
8262
+ // freshest-wins scan below only ever resolves the LAST occurrence.
8263
+ exactPrimaryStepIndex = null,
8264
+ // When given, restricts the matching-drill scan below to candidates whose
8265
+ // OWN endpoint (not a later chain hop) matches this key. A single primary
8266
+ // occurrence can structurally resolve MULTIPLE independent per-item
8267
+ // targets — one per distinct drill endpoint (e.g. a genuinely per-item
8268
+ // drill loop where each item's own request lands on the SAME endpoint
8269
+ // pattern but a distinct path/endpoint identity) — and this function's own
8270
+ // freshest-first scan below `break`s after the FIRST endpoint it resolves,
8271
+ // so an unrestricted call can only ever return an override for ONE of
8272
+ // them. Used by mergeSpecPlanOntoSamePrimary to resolve the declared
8273
+ // joinFields override independently for EACH structural target, keyed by
8274
+ // its own drill endpoint, instead of only the one this function's
8275
+ // freshest-first scan happens to land on first.
8276
+ restrictToDrillEndpointKey = null) {
8277
+ const rawPrimaryArrayPath = spec.resultsPath.split(".");
8195
8278
  const matchesFoldReturnEndpoint = compileFoldReturnEndpointMatcher(spec);
8196
8279
  // Built once and shared across every primaryStepIndex — see
8197
8280
  // buildRequestValueIndex's docstring. Lets each primary immediately tell
@@ -8201,10 +8284,14 @@ primaryIdentityAnchor = null) {
8201
8284
  const requestValueIndex = buildRequestValueIndex(actions);
8202
8285
  let freshestPlan = null;
8203
8286
  for (let primaryStepIndex = 0; primaryStepIndex < actions.length; primaryStepIndex++) {
8287
+ if (exactPrimaryStepIndex !== null && primaryStepIndex !== exactPrimaryStepIndex) {
8288
+ continue;
8289
+ }
8204
8290
  if (primaryIdentityAnchor !== null &&
8205
8291
  operationGroupKey(actions[primaryStepIndex].capture) !== primaryIdentityAnchor) {
8206
8292
  continue;
8207
8293
  }
8294
+ const primaryArrayPath = resolveDeclaredArrayPath(rawPrimaryArrayPath, actions[primaryStepIndex].capture.responseBody);
8208
8295
  const primaryItems = objectItemsAtPath(actions[primaryStepIndex].capture.responseBody, primaryArrayPath);
8209
8296
  if (!primaryItems)
8210
8297
  continue;
@@ -8229,7 +8316,9 @@ primaryIdentityAnchor = null) {
8229
8316
  // response-only fallback below exists to resolve.
8230
8317
  const matchingDrillStepIndices = [];
8231
8318
  for (let drillStepIndex = primaryStepIndex + 1; drillStepIndex < actions.length; drillStepIndex++) {
8232
- if (matchesFoldReturnEndpoint(actions[drillStepIndex].capture)) {
8319
+ if (matchesFoldReturnEndpoint(actions[drillStepIndex].capture) &&
8320
+ (restrictToDrillEndpointKey === null ||
8321
+ endpointKey(actions[drillStepIndex].capture.url) === restrictToDrillEndpointKey)) {
8233
8322
  matchingDrillStepIndices.push(drillStepIndex);
8234
8323
  }
8235
8324
  }
@@ -8291,7 +8380,7 @@ primaryIdentityAnchor = null) {
8291
8380
  if (spec.drillResultsPath === undefined) {
8292
8381
  return findObjectArrayFieldOrWholeObject(drill.capture.responseBody)?.path ?? null;
8293
8382
  }
8294
- const path = spec.drillResultsPath.split(".");
8383
+ const path = resolveDeclaredArrayPath(spec.drillResultsPath.split("."), drill.capture.responseBody);
8295
8384
  return objectItemsAtPath(drill.capture.responseBody, path) ? path : null;
8296
8385
  })();
8297
8386
  const { chain, chainArrayPath, chainTerminalIndex } = computeFoldChain(actions, entryIndex, drillArrayPath ?? []);
@@ -8363,6 +8452,26 @@ function isDescendantArrayPath(candidate, base) {
8363
8452
  * spec's own drill indices look independent, so the heuristic's wrong-level
8364
8453
  * guess never survives alongside the corrected one.
8365
8454
  */
8455
+ /**
8456
+ * The stable identity of the logical drill-down a {@link FoldTarget}
8457
+ * resolves onto: the endpoint of its chain's TERMINAL call (the response
8458
+ * actually folded — see {@link FoldTarget.chainTerminalIndex}) paired with
8459
+ * the specific array read off that response (see
8460
+ * {@link FoldTarget.chainArrayPath}). Neither piece alone is stable enough to
8461
+ * compare two independently-resolved targets by: `drillStepIndex` (the
8462
+ * chain's ENTRY point) can legitimately differ between the structural
8463
+ * heuristic's own request-threading resolution and a spec's
8464
+ * response-only/upstream-hop resolution even when both terminate at the same
8465
+ * call (see {@link buildFoldPlanFromSpec}'s `entryIndex` docstring); and the
8466
+ * terminal endpoint alone can collide when one response happens to carry TWO
8467
+ * unrelated per-item arrays for two entirely different primaries (e.g. a
8468
+ * combined status response naming both `entries` and `contracts`). Pairing
8469
+ * the terminal's endpoint with the specific array it reads disambiguates
8470
+ * both cases at once.
8471
+ */
8472
+ function foldTargetDrillIdentity(actions, target) {
8473
+ return `${endpointKey(actions[target.chainTerminalIndex].capture.url)}\u0000${target.chainArrayPath.join(".")}`;
8474
+ }
8366
8475
  function mergeSpecPlanOntoSamePrimary(structuralPlans, actions, foldReturnSpec,
8367
8476
  // See detectDrillDownFoldPlan's identical parameter. Threaded into this
8368
8477
  // function's own buildFoldPlanFromSpec call so a spec plan resolved here
@@ -8371,9 +8480,82 @@ function mergeSpecPlanOntoSamePrimary(structuralPlans, actions, foldReturnSpec,
8371
8480
  primaryIdentityAnchor = null) {
8372
8481
  if (foldReturnSpec === null)
8373
8482
  return [...structuralPlans];
8483
+ // Override joinFields on EVERY structural primary occurrence the spec can
8484
+ // independently resolve at its OWN primaryStepIndex — not just whichever
8485
+ // single occurrence the freshest-wins `specPlan` resolution below happens
8486
+ // to land on. A re-issued/paginated primary (same endpoint, multiple
8487
+ // captures) produces one independent structural plan per occurrence; each
8488
+ // must get the declared override, not just the freshest one.
8489
+ const structuralPlansWithPerOccurrenceOverrides = structuralPlans.map((plan) => {
8490
+ // Resolved once per structural TARGET (keyed on that target's own drill
8491
+ // endpoint), not once for the whole primary occurrence: buildFoldPlanFromSpec's
8492
+ // freshest-first scan `break`s after the first drill endpoint it resolves, so a
8493
+ // single unrestricted call can only ever produce an override for ONE of a
8494
+ // primary's structural targets even when it has several (e.g. a genuinely
8495
+ // per-item drill loop resolving one independent target per item, each at its
8496
+ // own endpoint). See buildFoldPlanFromSpec's restrictToDrillEndpointKey docstring.
8497
+ const matchesDeclaredEndpoint = compileFoldReturnEndpointMatcher(foldReturnSpec);
8498
+ const declaredPrimaryArrayPath = resolveDeclaredArrayPath(foldReturnSpec.resultsPath.split("."), actions[plan.primaryStepIndex].capture.responseBody);
8499
+ const mergedTargets = plan.targets.map((target) => {
8500
+ const ownSpecPlan = buildFoldPlanFromSpec(actions, foldReturnSpec, primaryIdentityAnchor, plan.primaryStepIndex, endpointKey(actions[target.drillStepIndex].capture.url));
8501
+ if (ownSpecPlan !== null &&
8502
+ JSON.stringify(ownSpecPlan.primaryArrayPath) === JSON.stringify(plan.primaryArrayPath)) {
8503
+ // ownSpecPlan was resolved with restrictToDrillEndpointKey pinned to
8504
+ // this target's OWN drill endpoint, so its single target (see
8505
+ // buildFoldPlanFromSpec's `targets: [{...}]` literal — it never
8506
+ // returns more than one) IS the spec's resolution of this restricted
8507
+ // endpoint by construction. Matching it back to `target` via
8508
+ // foldTargetDrillIdentity is not just redundant but actively wrong: a
8509
+ // multi-hop drill chain can make the spec's forward walk
8510
+ // (buildFoldPlanFromSpec's computeFoldChain resolution) land on a
8511
+ // different chainTerminalIndex/chainArrayPath than the structural
8512
+ // heuristic did for the exact same restricted endpoint, so the
8513
+ // identity lookup misses and the declared joinFields override is
8514
+ // silently dropped in favor of the heuristic's structural guess.
8515
+ const specTarget = ownSpecPlan.targets[0];
8516
+ return specTarget === undefined ? target : { ...target, joinFields: specTarget.joinFields };
8517
+ }
8518
+ // buildFoldPlanFromSpec came back null (or landed on a different
8519
+ // primaryArrayPath) because it couldn't resolve a
8520
+ // primaryMatchedItemIndex for this drill — the join value it names
8521
+ // sits on the drill response but isn't discoverable at that same
8522
+ // literal path on any primary item (a generation-time-only schema-
8523
+ // inference concern; see resolveSpecMatchedPrimaryItemIndexFromResponse).
8524
+ // That failure has nothing to do with whether the declared joinFields
8525
+ // should be trusted at RUNTIME: emitFoldMatchAndMergeLines only ever
8526
+ // reads target.joinFields (a field-name string), never
8527
+ // primaryMatchedItemIndex. Once the declared spec's own endpoint
8528
+ // pattern and resultsPath demonstrably match THIS already
8529
+ // structurally-resolved target, the declared joinFields must win over
8530
+ // the structural guess regardless of whether a representative item
8531
+ // could be picked.
8532
+ // Test against the chain's TERMINAL capture (where the fold array —
8533
+ // and thus the declared identifier field — actually lives), not the
8534
+ // structural heuristic's threading-entry `drillStepIndex`: on a
8535
+ // multi-hop chain the two can diverge (see foldTargetDrillIdentity's
8536
+ // docstring above), and a declared endpointPattern names the call
8537
+ // that holds the array, not whichever earlier hop some other field
8538
+ // happened to be threaded into.
8539
+ const targetMatchesDeclaredEndpoint = matchesDeclaredEndpoint(actions[target.chainTerminalIndex].capture);
8540
+ const primaryArrayPathMatchesSpec = JSON.stringify(declaredPrimaryArrayPath) === JSON.stringify(plan.primaryArrayPath);
8541
+ return targetMatchesDeclaredEndpoint && primaryArrayPathMatchesSpec
8542
+ ? { ...target, joinFields: foldReturnSpec.joinFields }
8543
+ : target;
8544
+ });
8545
+ // A spec-declared target the heuristic missed entirely (an independent
8546
+ // drill-down at a NEW endpoint) is still found via the original
8547
+ // unrestricted resolution, which is free to land on any endpoint.
8548
+ const ownSpecPlan = buildFoldPlanFromSpec(actions, foldReturnSpec, primaryIdentityAnchor, plan.primaryStepIndex);
8549
+ const existingDrillIdentities = new Set(plan.targets.map((target) => foldTargetDrillIdentity(actions, target)));
8550
+ const newTargets = ownSpecPlan !== null &&
8551
+ JSON.stringify(ownSpecPlan.primaryArrayPath) === JSON.stringify(plan.primaryArrayPath)
8552
+ ? ownSpecPlan.targets.filter((target) => !existingDrillIdentities.has(foldTargetDrillIdentity(actions, target)))
8553
+ : [];
8554
+ return { ...plan, targets: [...mergedTargets, ...newTargets] };
8555
+ });
8374
8556
  const specPlan = buildFoldPlanFromSpec(actions, foldReturnSpec, primaryIdentityAnchor);
8375
8557
  if (specPlan === null)
8376
- return [...structuralPlans];
8558
+ return structuralPlansWithPerOccurrenceOverrides;
8377
8559
  const specPrimaryEndpointKey = endpointKey(actions[specPlan.primaryStepIndex].capture.url);
8378
8560
  // Deliberately keyed on the RAW primaryStepIndex (not endpoint identity):
8379
8561
  // this branch trusts the structural target's own chain/drillArrayPath and
@@ -8383,7 +8565,7 @@ primaryIdentityAnchor = null) {
8383
8565
  // different index) may have matched a different item entirely, so it must
8384
8566
  // fall through to the wholesale-replace (sharedDrillPlan) or union
8385
8567
  // (consumedPrimarySteps) handling below instead of merging by identity.
8386
- const samePrimaryPlan = structuralPlans.find((plan) => plan.primaryStepIndex === specPlan.primaryStepIndex &&
8568
+ const samePrimaryPlan = structuralPlansWithPerOccurrenceOverrides.find((plan) => plan.primaryStepIndex === specPlan.primaryStepIndex &&
8387
8569
  JSON.stringify(plan.primaryArrayPath) === JSON.stringify(specPlan.primaryArrayPath));
8388
8570
  if (samePrimaryPlan !== undefined) {
8389
8571
  // A structural target serving the SAME drill-down the spec declares
@@ -8403,38 +8585,53 @@ primaryIdentityAnchor = null) {
8403
8585
  // but equally valid, occurrence of the SAME re-issued/paginated drill
8404
8586
  // endpoint — raw-index equality only happened to override when both
8405
8587
  // resolutions picked the identical occurrence, silently leaving every
8406
- // other occurrence's joinFields un-overridden.
8407
- const specTargetsByDrillEndpointKey = new Map(specPlan.targets.map((target) => [
8408
- endpointKey(actions[target.drillStepIndex].capture.url),
8409
- target,
8410
- ]));
8411
- return structuralPlans.map((plan) => {
8588
+ // other occurrence's joinFields un-overridden. Keyed on each target's
8589
+ // {@link foldTargetDrillIdentity} — the terminal endpoint paired with the
8590
+ // specific array it reads — rather than `drillStepIndex`: a spec's
8591
+ // declared join value often resolves only through an upstream entry hop
8592
+ // (e.g. a header-threaded token) that isn't the endpointPattern-matched
8593
+ // call itself, so its `drillStepIndex` (== that entry hop) can
8594
+ // legitimately differ from the structural heuristic's own
8595
+ // `drillStepIndex` (its own, possibly different, threading entry point)
8596
+ // even when both chains terminate at the exact same logical drill-down
8597
+ // call and array.
8598
+ const specTargetsByDrillIdentity = new Map(specPlan.targets.map((target) => [foldTargetDrillIdentity(actions, target), target]));
8599
+ return structuralPlansWithPerOccurrenceOverrides.map((plan) => {
8412
8600
  if (plan !== samePrimaryPlan)
8413
8601
  return plan;
8414
8602
  const mergedTargets = plan.targets.map((target) => {
8415
- const specTarget = specTargetsByDrillEndpointKey.get(endpointKey(actions[target.drillStepIndex].capture.url));
8603
+ const specTarget = specTargetsByDrillIdentity.get(foldTargetDrillIdentity(actions, target));
8416
8604
  return specTarget === undefined ? target : { ...target, joinFields: specTarget.joinFields };
8417
8605
  });
8418
- const existingDrillEndpointKeys = new Set(plan.targets.map((target) => endpointKey(actions[target.drillStepIndex].capture.url)));
8419
- const newTargets = specPlan.targets.filter((target) => !existingDrillEndpointKeys.has(endpointKey(actions[target.drillStepIndex].capture.url)));
8606
+ const existingDrillIdentities = new Set(plan.targets.map((target) => foldTargetDrillIdentity(actions, target)));
8607
+ const newTargets = specPlan.targets.filter((target) => !existingDrillIdentities.has(foldTargetDrillIdentity(actions, target)));
8420
8608
  return { ...plan, targets: [...mergedTargets, ...newTargets] };
8421
8609
  });
8422
8610
  }
8423
8611
  // No exact (primaryStepIndex, primaryArrayPath) match, but a structural
8424
8612
  // plan on the SAME primaryStepIndex may still have independently detected
8425
- // a target at the SAME drillStepIndex a spec target names — just at the
8426
- // wrong array level (e.g. a shallower structural guess that happens to
8427
- // share the spec's drill-down call). That structural target's own
8428
- // primaryArrayPath/drillArrayPath/joinFields describe the heuristic's
8429
- // guess, not the flow author's declaration, so the spec target replaces
8430
- // it wholesale rather than being discarded by the mismatched-array-path
8431
- // branch below or partially merged as if the array level matched.
8432
- const specDrillStepIndices = new Set(specPlan.targets.map((target) => target.drillStepIndex));
8433
- const sharedDrillPlan = structuralPlans.find((plan) => endpointKey(actions[plan.primaryStepIndex].capture.url) === specPrimaryEndpointKey &&
8434
- plan.targets.some((target) => specDrillStepIndices.has(target.drillStepIndex)));
8613
+ // a target at the SAME drill ENDPOINT+array a spec target names — just at
8614
+ // the wrong array level (e.g. a shallower structural guess that happens to
8615
+ // share the spec's drill-down call). Matched by {@link
8616
+ // foldTargetDrillIdentity} (see the samePrimaryPlan branch above for why
8617
+ // the terminal+array, not the entry `drillStepIndex` alone, is the stable
8618
+ // identity to compare on) — not raw drillStepIndex equality:
8619
+ // buildFoldPlanFromSpec's own entryIndex resolution and the structural
8620
+ // heuristic's request-threading scan can each land on a different, but
8621
+ // equally valid, occurrence/hop of the SAME endpoint (or the same single
8622
+ // occurrence reached via a different chain entry point), and raw-index
8623
+ // equality only happens to hold when both resolutions pick the identical
8624
+ // index. That structural target's own primaryArrayPath/drillArrayPath/
8625
+ // joinFields describe the heuristic's guess, not the flow author's
8626
+ // declaration, so the spec target replaces it wholesale rather than being
8627
+ // discarded by the mismatched-array-path branch below or partially merged
8628
+ // as if the array level matched.
8629
+ const specDrillIdentities = new Set(specPlan.targets.map((target) => foldTargetDrillIdentity(actions, target)));
8630
+ const sharedDrillPlan = structuralPlansWithPerOccurrenceOverrides.find((plan) => endpointKey(actions[plan.primaryStepIndex].capture.url) === specPrimaryEndpointKey &&
8631
+ plan.targets.some((target) => specDrillIdentities.has(foldTargetDrillIdentity(actions, target))));
8435
8632
  if (sharedDrillPlan !== undefined) {
8436
- const remainingTargets = sharedDrillPlan.targets.filter((target) => !specDrillStepIndices.has(target.drillStepIndex));
8437
- const otherPlans = structuralPlans.filter((plan) => plan !== sharedDrillPlan);
8633
+ const remainingTargets = sharedDrillPlan.targets.filter((target) => !specDrillIdentities.has(foldTargetDrillIdentity(actions, target)));
8634
+ const otherPlans = structuralPlansWithPerOccurrenceOverrides.filter((plan) => plan !== sharedDrillPlan);
8438
8635
  const keptStructuralPlans = remainingTargets.length === 0
8439
8636
  ? otherPlans
8440
8637
  : [...otherPlans, { ...sharedDrillPlan, targets: remainingTargets }];
@@ -8449,10 +8646,13 @@ primaryIdentityAnchor = null) {
8449
8646
  // the independence guard below would otherwise do, since a
8450
8647
  // level-correcting spec's own drill indices can look entirely
8451
8648
  // independent) or silently dropped by the mismatched-array-path checks.
8452
- const supersededShallowerPlan = structuralPlans.find((plan) => endpointKey(actions[plan.primaryStepIndex].capture.url) === specPrimaryEndpointKey &&
8649
+ const supersededShallowerPlan = structuralPlansWithPerOccurrenceOverrides.find((plan) => endpointKey(actions[plan.primaryStepIndex].capture.url) === specPrimaryEndpointKey &&
8453
8650
  isDescendantArrayPath(specPlan.primaryArrayPath, plan.primaryArrayPath));
8454
8651
  if (supersededShallowerPlan !== undefined) {
8455
- return [...structuralPlans.filter((plan) => plan !== supersededShallowerPlan), specPlan];
8652
+ return [
8653
+ ...structuralPlansWithPerOccurrenceOverrides.filter((plan) => plan !== supersededShallowerPlan),
8654
+ specPlan,
8655
+ ];
8456
8656
  }
8457
8657
  // Keyed by the (primaryStepIndex, primaryArrayPath) pair, not
8458
8658
  // primaryStepIndex alone — a structural plan only ever consumed ITS OWN
@@ -8469,7 +8669,7 @@ primaryIdentityAnchor = null) {
8469
8669
  // contends for.
8470
8670
  const consumedIndices = new Set();
8471
8671
  const consumedPrimarySteps = new Set();
8472
- for (const plan of structuralPlans) {
8672
+ for (const plan of structuralPlansWithPerOccurrenceOverrides) {
8473
8673
  const planPrimaryEndpointKey = endpointKey(actions[plan.primaryStepIndex].capture.url);
8474
8674
  consumedPrimarySteps.add(`${planPrimaryEndpointKey}:${JSON.stringify(plan.primaryArrayPath)}`);
8475
8675
  for (const target of plan.targets) {
@@ -8480,19 +8680,27 @@ primaryIdentityAnchor = null) {
8480
8680
  !consumedPrimarySteps.has(`${specPrimaryEndpointKey}:${JSON.stringify(specPlan.primaryArrayPath)}`) &&
8481
8681
  specPlan.targets.every((target) => target.chain.every((chainIndex) => !consumedIndices.has(chainIndex)));
8482
8682
  if (specConsumesOnlyItsOwnIndices)
8483
- return [...structuralPlans, specPlan];
8683
+ return [...structuralPlansWithPerOccurrenceOverrides, specPlan];
8484
8684
  // The spec's primary endpoint/array is already consumed by a structural
8485
8685
  // plan on a DIFFERENT capture of the same re-issued primary operation
8486
8686
  // (paginated primary), but its own target chains touch no index that plan
8487
8687
  // already depends on — this is the same identity, not a raw-index
8488
8688
  // coincidence, so union the spec's targets onto it rather than silently
8489
- // dropping a declaration the flow author explicitly wrote.
8490
- const sameIdentityPlan = structuralPlans.find((plan) => endpointKey(actions[plan.primaryStepIndex].capture.url) === specPrimaryEndpointKey &&
8689
+ // dropping a declaration the flow author explicitly wrote. Membership in
8690
+ // "already covered" is decided by {@link foldTargetDrillIdentity} (see the
8691
+ // samePrimaryPlan branch above for why the terminal+array, not the entry
8692
+ // `drillStepIndex` alone, is the stable identity) — not raw drillStepIndex
8693
+ // equality. A spec target sharing a structural target's drill identity is
8694
+ // a re-declaration of that SAME drill-down (handled as a wholesale replace
8695
+ // by sharedDrillPlan above), not an independent one to union — so this
8696
+ // only ever unions genuinely new endpoints onto an already-identified
8697
+ // primary.
8698
+ const sameIdentityPlan = structuralPlansWithPerOccurrenceOverrides.find((plan) => endpointKey(actions[plan.primaryStepIndex].capture.url) === specPrimaryEndpointKey &&
8491
8699
  JSON.stringify(plan.primaryArrayPath) === JSON.stringify(specPlan.primaryArrayPath) &&
8492
- specPlan.targets.every((target) => !plan.targets.some((t) => t.drillStepIndex === target.drillStepIndex)));
8700
+ specPlan.targets.every((target) => !plan.targets.some((t) => foldTargetDrillIdentity(actions, t) === foldTargetDrillIdentity(actions, target))));
8493
8701
  if (sameIdentityPlan === undefined)
8494
- return [...structuralPlans];
8495
- return structuralPlans.map((plan) => plan === sameIdentityPlan ? { ...plan, targets: [...plan.targets, ...specPlan.targets] } : plan);
8702
+ return [...structuralPlansWithPerOccurrenceOverrides];
8703
+ return structuralPlansWithPerOccurrenceOverrides.map((plan) => plan === sameIdentityPlan ? { ...plan, targets: [...plan.targets, ...specPlan.targets] } : plan);
8496
8704
  }
8497
8705
  /**
8498
8706
  * The single fold-plan entry point: {@link detectDrillDownFoldPlan}'s