Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1c473bc
feat: holdout support — normalized models, assignment precedence, exp…
marcio-absmartly Jul 4, 2026
3e5318a
fix: post-restore review findings
marcio-absmartly Jul 4, 2026
c85fd0f
refactor: simplifier pass for holdouts
marcio-absmartly Jul 4, 2026
0efefdc
style: spotless formatting
marcio-absmartly Jul 5, 2026
5af036a
fix: address fable-5 review findings
marcio-absmartly Jul 9, 2026
5ec87d4
fix: reuse cached forced-variant assignments with custom assignments
marcio-absmartly Jul 10, 2026
d15f26e
docs: document shared-array immutability invariant
marcio-absmartly Jul 10, 2026
b2e54ed
fix: address gpt-5.6-sol review findings
marcio-absmartly Jul 12, 2026
c2a8338
fix: revert VariantAssigner normalizer change to preserve collector h…
marcio-absmartly Jul 13, 2026
eb7dd64
feat: support full_on holdouts
marcio-absmartly Jul 16, 2026
4fc32ad
refactor: revert holdout wire model to first-class experiment shape
marcio-absmartly Aug 8, 2026
386a80c
feat: enforce holdout suppression and unified holdout exposure emission
marcio-absmartly Aug 9, 2026
f0d99e0
test: rewrite holdout contract tests for the first-class experiment m…
marcio-absmartly Aug 9, 2026
6c3a4f4
fix: pin holdout cache identity to (id, iteration) to stop exposure t…
marcio-absmartly Aug 9, 2026
959286b
fix: a suppressed assignment is not a participant in its experiment
marcio-absmartly Aug 9, 2026
0395e7e
fix: read units_ under contextLock_ and avoid write lock on holdout c…
marcio-absmartly Aug 9, 2026
1eaeb4b
fix: normalize excludedExperimentIds and preserve holdout triggers on…
marcio-absmartly Aug 9, 2026
28b31d1
test: prove the holdout union rule and harden malformed/missing-unit …
marcio-absmartly Aug 9, 2026
b886623
refactor: drop redundant suppressed check and rename exposure trigger…
marcio-absmartly Aug 9, 2026
66304fb
fix: pin covered-experiment suppression to the same HoldoutAssignment…
marcio-absmartly Aug 21, 2026
2f88bfb
fix: trigger applicable holdouts for every evaluated variable-key can…
marcio-absmartly Aug 21, 2026
db9818f
fix: attach applicable holdouts to overridden assignments
marcio-absmartly Aug 21, 2026
b63532e
test: collapse identical variant ternary in holdout experiment factory
marcio-absmartly Aug 21, 2026
b961b15
fix: resolve holdout definition from live data before pinned-cache ch…
marcio-absmartly Aug 22, 2026
5c090ec
fix: revalidate applicable-holdout coverage on the override fast path
marcio-absmartly Aug 22, 2026
5233889
fix: schedule a publish flush for every enqueued exposure
marcio-absmartly Aug 22, 2026
070c775
fix: sort a defensive copy of excludedExperimentIds instead of the wi…
marcio-absmartly Aug 22, 2026
83551ca
feat: replace holdout-side excludedExperimentIds with per-experiment …
marcio-absmartly Aug 22, 2026
362b3d5
test: re-encode holdout fixtures as per-experiment holdoutIds
marcio-absmartly Aug 22, 2026
7fce871
test: cover malformed-holdout filtering via explicit refs, drop revie…
marcio-absmartly Aug 22, 2026
23072b7
refactor: simplify holdout resolution/test machinery
marcio-absmartly Aug 22, 2026
6a15eb9
feat: resolve holdout applicability from arm count instead of collect…
marcio-absmartly Aug 28, 2026
beb943f
test: cover two- and three-arm holdout assignment
marcio-absmartly Aug 28, 2026
14c905f
fix: pin holdout arm count with the cached arm so refreshes cannot re…
marcio-absmartly Aug 28, 2026
07c0ae7
refactor: simplify holdout arm resolution
marcio-absmartly Aug 28, 2026
55b87fe
fix: trigger holdout exposures from the assignments used for the deci…
marcio-absmartly Aug 31, 2026
0f64b81
fix: schedule the queued exposure even when the event logger throws
marcio-absmartly Aug 31, 2026
c6a0a5a
fix: evict null holdout snapshots on setUnit so a late unit is not lost
marcio-absmartly Aug 31, 2026
dc2afeb
test: close the surviving null-entry eviction mutant on setUnit
marcio-absmartly Aug 31, 2026
c3bd855
refactor: simplify late-unit invalidation coverage
marcio-absmartly Aug 31, 2026
9712032
docs: keep the degraded-record caveat on the exposed guard
marcio-absmartly Aug 31, 2026
5d0650e
fix: settle exposure state across late-unit retries
marcio-absmartly Sep 1, 2026
24ca2a8
Merge branch 'main' into feat/holdouts
marcio-absmartly Sep 1, 2026
7d9ea90
test: replace stale audience cache signal
marcio-absmartly Sep 1, 2026
ac76466
style: fix import order flagged by spotless
marcio-absmartly Sep 1, 2026
b05cb26
fix: narrow custom-assignment cache invalidation to the pinned custom…
marcio-absmartly Sep 2, 2026
1b2501c
fix: key holdout assignment cache by (id, effective unit type)
marcio-absmartly Sep 2, 2026
6a64267
fix: use the bundled Objects shim to hold the Java 1.6 API floor
marcio-absmartly Sep 2, 2026
013a800
test: stop requiring holdoutType in the deserializer fixture
marcio-absmartly Sep 2, 2026
944f700
test: pin both the slim and legacy holdout wire shapes
marcio-absmartly Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
664 changes: 595 additions & 69 deletions core-api/src/main/java/com/absmartly/sdk/Context.java

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions core-api/src/main/java/com/absmartly/sdk/json/ContextData.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public class ContextData {
public Experiment[] experiments = new Experiment[0];
public Experiment[] holdouts = new Experiment[0];

public ContextData() {}

Expand All @@ -19,25 +20,34 @@ public ContextData(Experiment[] experiments) {
this.experiments = experiments;
}

@SuppressFBWarnings(value = "EI_EXPOSE_REP2")
public ContextData(Experiment[] experiments, Experiment[] holdouts) {
this.experiments = experiments;
this.holdouts = holdouts;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ContextData that = (ContextData) o;
return Arrays.equals(experiments, that.experiments);
return Arrays.equals(experiments, that.experiments) && Arrays.equals(holdouts, that.holdouts);
}

@Override
public int hashCode() {
return Arrays.hashCode(experiments);
int result = Arrays.hashCode(experiments);
result = 31 * result + Arrays.hashCode(holdouts);
return result;
}

@Override
public String toString() {
return "ContextData{" +
"experiments=" + Arrays.toString(experiments) +
", holdouts=" + Arrays.toString(holdouts) +
'}';
}
}
18 changes: 17 additions & 1 deletion core-api/src/main/java/com/absmartly/sdk/json/Experiment.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ public class Experiment {
public String audience;
public CustomFieldValue[] customFieldValues;

// Set only for entries served in the top-level `holdouts` array; null for ordinary experiments.
public String holdoutType;

// Set only for entries served in the top-level `experiments` array: the ids of the holdouts
// (from the top-level `holdouts` array) that cover this experiment. Null or empty means the
// experiment is not covered by any holdout. Absent on holdout entries themselves.
public int[] holdoutIds;

public Experiment() {}

@Override
Expand Down Expand Up @@ -66,7 +74,11 @@ public boolean equals(Object o) {
return false;
if (audience != null ? !audience.equals(that.audience) : that.audience != null)
return false;
return Arrays.equals(customFieldValues, that.customFieldValues);
if (!Arrays.equals(customFieldValues, that.customFieldValues))
return false;
if (holdoutType != null ? !holdoutType.equals(that.holdoutType) : that.holdoutType != null)
return false;
return Arrays.equals(holdoutIds, that.holdoutIds);
}

@Override
Expand All @@ -87,6 +99,8 @@ public int hashCode() {
result = 31 * result + (audienceStrict ? 1 : 0);
result = 31 * result + (audience != null ? audience.hashCode() : 0);
result = 31 * result + Arrays.hashCode(customFieldValues);
result = 31 * result + (holdoutType != null ? holdoutType.hashCode() : 0);
result = 31 * result + Arrays.hashCode(holdoutIds);
return result;
}

Expand All @@ -109,6 +123,8 @@ public String toString() {
", audienceStrict=" + audienceStrict +
", audience='" + audience + '\'' +
", customFieldValues=" + Arrays.toString(customFieldValues) +
", holdoutType='" + holdoutType + '\'' +
", holdoutIds=" + Arrays.toString(holdoutIds) +
'}';
}
}
Loading
Loading