Skip to content

feat(bucketing): anchored layout (contract v12) — JS SDK reference impl#414

Merged
abbaseya merged 11 commits into
feat/fullstack-v12from
feat/anchored-bucketing-layout
Jul 7, 2026
Merged

feat(bucketing): anchored layout (contract v12) — JS SDK reference impl#414
abbaseya merged 11 commits into
feat/fullstack-v12from
feat/anchored-bucketing-layout

Conversation

@abbaseya

@abbaseya abbaseya commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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 = 11 today, so no served experience carries version > 11 until 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 (+333 at each arm's own edge), never a reshuffle.

What changed

  • @convertcom/js-sdk-types — new exported VariationAllocation type ({ id, allocation, active }).
  • @convertcom/js-sdk-bucketing — new getBucketRanges, selectBucketAnchored, getBucketForVisitorAnchored (+ BucketAnchoredRange type) implementing the normative anchored algorithm (IEEE754 doubles, no rounding; totalWeight over all arms incl. inactive; inactive arms keep weight + zero width; isNaN(ta)→100.0; totalWeight<=0→null; boundary anchor <= value < anchor+width). Packed path frozen.
  • @convertcom/js-sdk-data — gate in DataManager._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.
  • Canonical fixturepackages/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

  • [BUCK-1] VariationAllocation type + anchored interface signatures
  • [BUCK-2] Anchored algorithm in js-sdk-bucketing
  • [DATA-1] version > 11 gate in DataManager._retrieveBucketing
  • [GOLD-1] Canonical cross-SDK golden-vector fixture + runner
  • [FIX-R1] Review fix — corrected stale TDD comments in the new test files

Tests (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 public Context.runExperience chain, proving browser↔Node parity. Full monorepo build green; SonarCloud new_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; redistribute untouched. Rollout items — the backend CURRENT_EXPERIENCE_VERSION 11→12 bump, tracking-script vendored bump, UI creation-time warnings, shared bucketing-algorithm.md doc — are separate tasks.

🤖 Generated with Claude Code

@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 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.

Comment thread packages/data/src/data-manager.ts
Comment thread packages/bucketing/src/bucketing-manager.ts
abbaseya and others added 7 commits July 3, 2026 20:39
…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>
@abbaseya
abbaseya force-pushed the feat/anchored-bucketing-layout branch from 34c0fef to f472916 Compare July 3, 2026 17:39
@abbaseya
abbaseya changed the base branch from main to main-convert July 3, 2026 17:39
@abbaseya abbaseya self-assigned this Jul 3, 2026
@abbaseya
abbaseya requested a review from a team July 3, 2026 17:43
abbaseya and others added 3 commits July 3, 2026 20:54
…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.
@abbaseya
abbaseya changed the base branch from main-convert to feat/fullstack-v12 July 4, 2026 15:03

@JosephSamirL JosephSamirL left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_buildPackedBuckets is a verbatim extraction of the old inline builder; the entire deletion surface is that extraction plus import reshuffles, nothing behavioral.
  • Anchored algorithm_buildVariationAllocations + getBucketRanges + selectBucketAnchored implement the normative algorithm (isNaN/absent→100, active = (status nil||RUNNING) && (ta>0||isNaN), totalWeight over 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; bucketingHashOptions is computed once and shared; getBucketForVisitorAnchored reuses getValueVisitorBased; 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)

  1. Title + body still say "contract v9" / version > 8 / "stamping 9", but the code gates on > 11 (contract v12) — FIX-R3 corrected the gate, not the narrative. The fixture has 19 v11 = packed vectors, so anyone implementing the body's literal > 8 would route v11 → anchored and fail them. Please update: title → v12, body → > 11 / stamps 11→12 / version ≤ 11.
  2. "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. ✅

@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@abbaseya abbaseya changed the title feat(bucketing): anchored layout (contract v9) — JS SDK reference impl feat(bucketing): anchored layout (contract v12) — JS SDK reference impl Jul 6, 2026
@abbaseya
abbaseya merged commit 24ec631 into feat/fullstack-v12 Jul 7, 2026
1 check passed
@abbaseya
abbaseya deleted the feat/anchored-bucketing-layout branch July 7, 2026 03:20
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