Skip to content

feat!: anchored bucketing layout for traffic ramping (contract v12)#41

Merged
JosephSamirL merged 5 commits into
feat/fullstack-v12from
feat/anchored-bucketing-layout
Jul 6, 2026
Merged

feat!: anchored bucketing layout for traffic ramping (contract v12)#41
JosephSamirL merged 5 commits into
feat/fullstack-v12from
feat/anchored-bucketing-layout

Conversation

@abbaseya

@abbaseya abbaseya commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the anchored bucketing layout as an extra pass beside the existing packed walk, selected per experience by the served experience.version field: version > 11 → anchored; version ≤ 11 / missing / non-numeric → packed (bit-for-bit as today).

This is a breaking change to the cross-SDK bucketing contract (v12) but ships INERT — the backend currently stamps every experience version 11, so no served experience triggers the anchored path until a separate backend bump to 12. Hash, seed (9999), scaling, stored-decision guard, forced-variation path, and event payloads are all unchanged.

Under the anchored layout each variation occupies a band anchored to its cumulative position, so raising an experiment's total traffic admits new visitors at each arm's own edge instead of relocating existing assignments (fixes the packed-layout reallocation defect). The JS SDK is the cross-SDK parity oracle; equality is proven by a shared golden-vector fixture.

Changes

  • AnchoredBucketing.swift — new anchored selector + allocation builder (config-order allocations, isNaN/absent → 100.0, active/inactive with weight-preserving zero-width inactive arms, half-open [anchor, anchor+width)), a faithful port of the JS reference.
  • BucketingManager.swift — new bucketVersionGated version-gated entry point; packed eligible builder now defaults an omitted/NaN traffic_allocation to 100.0 to match the JS packed builder (the only packed-path change; the accumulate-first-wins walk is untouched).
  • ExperienceManager.swiftselectVariation now routes bucketing through bucketVersionGated (sticky short-circuit still precedes it, so stored-decision precedence is unchanged).
  • Tests + the 59-vector cross-SDK golden fixture (cross-sdk-bucketing-vectors.json, shared verbatim across all Convert SDKs).

Tests

  • swift test337/337 passing.
  • AnchoredBucketingParity59/59 golden vectors (19 v11 packed-regression + 40 v12 anchored) — AC7.
  • AC1 gate branching, AC4/AC5 boundaries & defaults, AC9 event-shape — passing.
  • Existing cross-SDK HashParity (74 cases) green unchanged; packed path bit-identical for version ≤ 11 (AC6).
  • SwiftLint --strict: 0 violations.

Notes

  • No OpenAPI spec — SDK-internal behavioral change gated on a config field the served config already carries; no API endpoint added or modified.
  • This SDK pass is independent of the other SDK passes — consistency is enforced solely by the shared golden-vector fixture, not by merge sequencing.

Coordination

The backend's next CURRENT_EXPERIENCE_VERSION bump to 12 MUST be the anchored-rollout bump (an unrelated bump to 12 would activate anchored prematurely). Safe deploy order is SDK-first.

abbaseya and others added 4 commits July 4, 2026 05:38
Beads: ai-driven-product-dev-0etc
Adds the 59-vector cross-SDK bucketing fixture (19 v11 packed-regression +
40 v12 anchored) shared verbatim across all Convert SDKs. sha256
9993d0e6c5719cb17a4dcbcc73af9c0cb14ee0258e7fd29cc88f44b61083dd1b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-t6in
Adds the anchored parity suite (59-vector sweep) + AC1-AC9 gate/boundary tests
and a deliberately-unimplemented AnchoredBucketing.selectBucket stub so the
target compiles and the new assertions fail. Packed bucket()/selectBucket
untouched (additive only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an anchored bucketing layout selected per experience by the served
`experience.version` field. When `version` is greater than 11 the SDK buckets
visitors under the anchored layout; experiences at version 11 or below (and
those with a missing or non-numeric version) continue to use the existing
packed layout, bit for bit.

Under the anchored layout each variation occupies a band anchored to its
cumulative position in the allocation space rather than being packed end to
end. Raising an experiment's total traffic allocation therefore admits new
visitors at each arm's own edge and never relocates a visitor who was already
bucketed, and lowering allocation ejects out-of-range visitors without ever
flipping a retained visitor to a different arm. Stopped arms and arms with an
explicit zero allocation keep their anchor position but receive zero width.

The hash, seed, scaling, stored-decision precedence, forced-variation path,
and event payloads are unchanged.

BREAKING CHANGE: bumps the cross-SDK bucketing contract to v12. Experiences
served with `version` greater than 11 are bucketed with the anchored layout,
which assigns different variations than the previous packed layout for the
same visitor. Experiences at version 11 or below are unaffected.
…tation

Beads: ai-driven-product-dev-t6in
Removes stale Phase-1 RED/stub framing from the anchored test doc comments so
they describe the current implemented behavior. Comment-only; no assertions or
production code changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@abbaseya abbaseya self-assigned this Jul 4, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the anchored bucketing layout (contract v12+) via the new AnchoredBucketing module, gates its usage in BucketingManager based on the experience version, and updates ExperienceManager to route through this version gate. It also adds extensive test suites and JSON fixtures to ensure cross-SDK parity and boundary correctness. The review feedback suggests simplifying the allocation logic in AnchoredBucketing to improve consistency and readability, as well as removing redundant nil-coalescing operations in BucketingManager by directly utilizing the guaranteed non-nil selectedId.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread Sources/ConvertSwiftSDKCore/Bucketing/AnchoredBucketing.swift Outdated
Comment thread Sources/ConvertSwiftSDKCore/Bucketing/BucketingManager.swift
Comment thread Sources/ConvertSwiftSDKCore/Bucketing/BucketingManager.swift Outdated
…id directly

Beads: ai-driven-product-dev-t6in
Addresses code-review feedback (behavior-preserving):
- buildAllocations: drop the redundant isDefaulted term (allocation>0 already
  covers the NaN/absent->100 case) and use the if-let default form.
- bucketVersionGated: use the resolved selectedId directly instead of
  selected.id ?? "" (the arm was matched on id, so it is guaranteed equal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@abbaseya
abbaseya requested a review from a team July 4, 2026 05:07
@abbaseya
abbaseya changed the base branch from main to feat/fullstack-v12 July 4, 2026 15:03

@JosephSamirL JosephSamirL left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

Reviewed the anchored bucketing layout end-to-end and it holds up:

  • Anchored algorithm is correct — recomputed the golden bands by hand (thirds 15%/25%, stopped-arm stability, NaN-default, ta:0 zero-width, total-weight-zero); all match, including the total ≠ 100 "sliver" growth and the half-open [anchor, anchor+width) edges.
  • Cross-SDK parity confirmedcross-sdk-bucketing-vectors.json is byte-identical (sha256 9993d0e6…83dd1b) to the JS reference in convertcom/javascript-sdk#414, the version gate matches (experience.version > 11), and the port mirrors the JS _buildVariationAllocations / getBucketRanges / selectBucketAnchored line-for-line.
  • Determinism preserved — MurmurHash3, seed, and the stored-decision short-circuit are untouched; the anchored path re-derives the bucket value byte-for-byte identically to the packed path, and the event shape (one .bucketing, segments: nil) is unchanged.
  • Gate coverage is complete — every production path (runExperience(s), and runFeature(s) via FeatureManager) routes through ExperienceManager.selectVariationbucketVersionGated; nothing bypasses it.
  • Anchored layout ships inert — it can't activate while the backend stamps CURRENT_EXPERIENCE_VERSION = 11.
  • Ran swift test locally (Swift 6.2.3): 337 tests / 67 suites pass, 0 failures, including the 59-vector parity sweep and the AC gate/boundary suites.

Nice work on the sha-pinned shared fixture and the parameterized suites. ✅

@JosephSamirL
JosephSamirL merged commit 8c1dddf into feat/fullstack-v12 Jul 6, 2026
10 checks passed
@JosephSamirL
JosephSamirL deleted the feat/anchored-bucketing-layout branch July 6, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants