feat(bucketing): anchored layout (contract v12) — JS SDK reference impl#414
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements an anchored bucketing layout (contract v9) alongside the existing packed layout, gated by experience.version > 8. It introduces core layout calculation, selection, and visitor-bucketing methods to BucketingManager, integrated via DataManager, and includes comprehensive unit tests and cross-SDK golden-vector parity tests. The review feedback identifies high-severity issues where negative traffic allocations could corrupt the layout math and shift anchors, recommending defensive clamping to 0 in both _buildVariationAllocations and getBucketRanges.
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.
…face signatures Beads: ai-driven-product-dev-p5at Agent: sdk-js Foundation only — no algorithm/behavior change. Build+lint green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-n7x9 18 Mocha/Chai tests, spec-derived expected numbers as independent oracle. AC2/AC4/AC5 coverage. Fail cleanly (methods not implemented yet). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-n7x9 getBucketRanges/selectBucketAnchored/getBucketForVisitorAnchored per normative spec. Packed path frozen. 28/28 bucketing tests pass (18 anchored + 10 packed), TS2420 self-resolved on default yarn test:mocha. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-pm33 9 Mocha/Chai tests: AC1 gate branching, AC4 stops/ta:0, AC8 guard precedence, AC9 no schema drift. 4 fail (gate not implemented), 5 pass (packed-path + invariant locks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-pm33 Number(experience.version) > 8 -> anchored (via _buildVariationAllocations + getBucketForVisitorAnchored); else packed unchanged (_buildPackedBuckets extracted verbatim). Guard/hash/seed/event payloads untouched. 37/37 data suite pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + runner Beads: ai-driven-product-dev-jnz9 59 vectors (all spec categories: incident-flip, raise-superset, per-sliver, lower-eject, stopped-arm, ta:0, NaN, boundary, v8===v9 coincidence). Expected values computed then frozen; every spec-given band matched. Runner 60 passing. Full monorepo suite green (480 Mocha + 36 Playwright). Cross-SDK contract for the 5 sibling SDKs; fixture at packages/bucketing/tests/cross-sdk-bucketing-vectors.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d test files Beads: ai-driven-product-dev-kz6x Review R1 (conf 95): header comments claimed methods/gate not-yet-implemented (RED); now shipped + passing. Rewrote to describe shipped behavior for sibling-SDK porters; fixed stale data-manager.ts:620-637 -> :685 gate ref. Comment-only. Suites unchanged (bucketing 28, data 97). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
34c0fef to
f472916
Compare
…SonarCloud S2871 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent stamp is 11) + browser golden-vector test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erience version Introduces an anchored bucketing layout chosen per experience by experience.version: experiences at version > 11 bucket via the anchored layout, while version <= 11, missing, or non-numeric keep the existing packed walk unchanged. Anchored ranges stay fixed to each variation's position, so raising traffic allocation grows every arm in place instead of relocating ranges and re-bucketing already-assigned visitors. Adds public API to @convertcom/js-sdk-bucketing (getBucketRanges, selectBucketAnchored, getBucketForVisitorAnchored) and a VariationAllocation type in @convertcom/js-sdk-types. Hash, seed, stored-decision guard, forced-variation path, and event payloads are unchanged. BREAKING CHANGE: bucketing layout is now selected by experience.version; experiences at version > 11 use the new anchored layout instead of the packed cumulative walk.
JosephSamirL
left a comment
There was a problem hiding this comment.
Approved ✅ — the implementation is correct, CI is green (incl. SonarCloud's 3% gate), and it matches the cross-SDK contract. One docs-only fix required before this is treated as the reference the other SDKs mirror (below).
Verified correct
- Gate
Number(experience.version) > 11— matches the fixture's v11-packed / v12-anchored split and the iOS SDK (ios-sdk#41). - Packed path bit-for-bit unchanged —
_buildPackedBucketsis a verbatim extraction of the old inline builder; the entire deletion surface is that extraction plus import reshuffles, nothing behavioral. - Anchored algorithm —
_buildVariationAllocations+getBucketRanges+selectBucketAnchoredimplement the normative algorithm (isNaN/absent→100,active = (status nil||RUNNING) && (ta>0||isNaN),totalWeightover all arms incl. inactive,anchor = (cum/total)*10000,width = active ? alloc*100 : 0, half-open[anchor, anchor+width)) — byte-identical to the iOS port. - Determinism / guards preserved — the gate sits in the fresh-bucketing branch after the forced/stored-decision guards;
bucketingHashOptionsis computed once and shared;getBucketForVisitorAnchoredreusesgetValueVisitorBased; hash and seed untouched. - Fixture — sha256
9993d0e6…83dd1b, 59 vectors (19 v11 + 40 v12); ios-sdk#41 consumes it byte-for-byte (verified).
Please fix before merge (docs only — this PR is the cross-SDK reference)
- Title + body still say "contract v9" /
version > 8/ "stamping 9", but the code gates on> 11(contract v12) —FIX-R3corrected the gate, not the narrative. The fixture has 19 v11 = packed vectors, so anyone implementing the body's literal> 8would route v11 → anchored and fail them. Please update: title → v12, body →> 11/ stamps 11→12 /version ≤ 11. - "contract v9" also lingers in code comments / test descriptions — e.g.
data-manager.ts: "the anchored (contract v9) layout … activates starting at experience version 12" (contradictory in one sentence). Rename v9 → v12 to match iOS.
Ran the cross-SDK parity check against ios-sdk#41 — fixture byte-identical, gate matches. Clean TDD history. ✅
|



Summary
Adds the anchored bucketing layout (bucketing contract v12) as an extra pass beside the existing packed walk, gated per experience on
Number(experience.version) > 11. This is the JS SDK reference implementation and authors the canonical cross-SDK golden-vector fixture the five sibling SDKs (PHP, Python, Ruby, Android, iOS) will consume verbatim.Ships inert: the backend stamps every new experience with
CURRENT_EXPERIENCE_VERSION = 11today, so no served experience carriesversion > 11until the backend bumps that constant from 11 to 12 (separate, out-of-scope task). SDK-first deploy is intentional and safe. The packed path is bit-for-bit unchanged; hash, seed,redistribute, stored-decision guard, forced-variation path, and event payloads are all untouched.Why anchored
The packed layout lays variation ranges end-to-end; raising total allocation mid-flight opens one new band at the open end belonging to the last variation. Customer-verified incident (Distilled.ie): raising 15% → 25% on a 3-arm experiment moved daily bucketings from even thirds to
3,171 / 4,047 / 7,463. Anchored ranges keep each arm pinned at its own edge — a raise is a superset (+333at each arm's own edge), never a reshuffle.What changed
@convertcom/js-sdk-types— new exportedVariationAllocationtype ({ id, allocation, active }).@convertcom/js-sdk-bucketing— newgetBucketRanges,selectBucketAnchored,getBucketForVisitorAnchored(+BucketAnchoredRangetype) implementing the normative anchored algorithm (IEEE754 doubles, no rounding;totalWeightover all arms incl. inactive; inactive arms keep weight + zero width;isNaN(ta)→100.0;totalWeight<=0→null; boundaryanchor <= value < anchor+width). Packed path frozen.@convertcom/js-sdk-data— gate inDataManager._retrieveBucketing's fresh-bucketing branch (after the forced/stored-decision guards):version > 11→ anchored via_buildVariationAllocations; else the existing packed path (_buildPackedBuckets, extracted verbatim). No event/schema drift.packages/bucketing/tests/cross-sdk-bucketing-vectors.json(59 vectors: 19 v11-packed + 40 v12-anchored) + an end-to-end runner. Expected values computed then frozen; every spec-given band re-derived by hand and matched.Tasks completed
VariationAllocationtype + anchored interface signaturesjs-sdk-bucketingversion > 11gate inDataManager._retrieveBucketingTests (Mocha + Chai, + Playwright browser)
Covers AC1–AC9, including the v11 → packed / v12 → anchored gate boundary. The golden vectors also run in a real headless browser (
packages/js-sdk/tests/browser/golden-vectors.spec.ts) through the publicContext.runExperiencechain, proving browser↔Node parity. Full monorepo build green; SonarCloudnew_duplicated_lines_density ≤ 3%gate respected via hoisted fixtures + parameterized cases.Non-goals (not in this PR)
No variation-weight-change support; no retrofit of
version ≤ 11; no server-computed ranges;redistributeuntouched. Rollout items — the backendCURRENT_EXPERIENCE_VERSION11→12 bump, tracking-script vendored bump, UI creation-time warnings, sharedbucketing-algorithm.mddoc — are separate tasks.🤖 Generated with Claude Code