Skip to content

feat(rules): bucketed_into_experience_key mutual-exclusion audience rule (qs-03)#43

Merged
JosephSamirL merged 14 commits into
feat/fullstack-v12from
feat/mutual-exclusion
Jul 16, 2026
Merged

feat(rules): bucketed_into_experience_key mutual-exclusion audience rule (qs-03)#43
JosephSamirL merged 14 commits into
feat/fullstack-v12from
feat/mutual-exclusion

Conversation

@abbaseya

@abbaseya abbaseya commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the additive audience rule type bucketed_into_experience_key to the iOS SDK, enabling declarative mutual exclusion on fullstack, and aligns it to the JavaScript SDK reference (#416): an audience whose value is a target experience KEY matches iff the visitor is already bucketed into that experience per the SDK's stored (id-keyed) visitor bucketing state, combining with negated: true to express "NOT in experiment X". Read-only, evaluated before bucketing — never computes hashes, never writes, never tracks. Zero new application inputs. Cross-session exclusion is automatic via native persistence.

Spec of record: _bmad-output/implementation-artifacts/2026-06-09-convert-ios-sdk/qs-04-mutual-exclusion-rule.md (issue #86).

Stacked PR — base is feat/fullstack-v12 (NOT main).

Model (bit-faithful to javascript-sdk data-manager.ts)

  • Whole-audience-override exclusion — an audience containing any bucketed_into_experience_key leaf is an exclusion audience; its whole result is the exclusion resolution and sibling leaves are NOT evaluated (mirrors _isBucketingExclusionRule + filterMatchedRecordsWithRule). Resolution (BucketingExclusion.resolve) mirrors _resolveBucketingExclusion: target by KEY → id-keyed membership in stored bucketing → unknown target warns + false → negation applied last. Read-only.
  • matching_options.audiences ALL/ANY at the experience level (mirrors matchRulesByField): each attached audience is matched independently (exclusion via override, generic via the rule engine), then composed — ALL ⇒ every audience must match; ANY/absent ⇒ at least one; empty ⇒ unrestricted. (Previously iOS OR-ed all audiences and ignored the field, making the spec's recommended ALL safety config a no-op.)

What changed (all in ConvertSwiftSDKCore, no generated code)

  • Decode-survival (M1, retained): a hand-authored per-audience degrading decode (ProjectConfig+AudienceDecoding.swift) — the generated RuleElementAudience throws on the unknown discriminator; without this, a whole-array try? would null all audiences → a project-wide fail-OPEN. Now a bad audience degrades alone (siblings survive), its tree sentinel-captured into degradedAudienceSentinels.
  • Exclusion seam: Rules/BucketingExclusion.swift (new) + Experience/ExperienceManager+MutualExclusion.swift detection (statefulLeaf) + ExperienceManager.audiencePasses reworked to per-audience match + matching_options composition.
  • Simplification: RuleManager.evaluate dropped its in-tree resolver param and stateful branch — back to a generic-only evaluator (generic path bit-identical, AC7). Its header comment was corrected to not overclaim rule-manager.ts parity (holds only for single-leaf OR_WHEN; references the filed OR_WHEN-collapse issue).

Acceptance criteria (all green)

AC1 8-row fixture (frozen) · AC2 end-to-end exclusion · AC3 cross-relaunch persistence · AC4 empty attributes · AC5 read-only (spied) · AC6 two-audience combination under ALL and ANY (reworked to the JS #416 shape — a dedicated exclusion audience + a generic audience) · AC7 generic-rule regression lock (typed + JSON paths bit-identical) · AC8 unknown-target warning names the key.

Quality gates

  • Decision audit PASS (independent producer→refuter; the auditor re-ran the suites and confirmed JS #416 bit-faithfulness).
  • Code review clean.
  • swift build clean; ConvertSwiftSDKCore suite green (217 tests); repo-wide swiftlint --strict clean; generated code byte-unchanged; ruleNegation Bool knob unchanged.

Filed separately (pre-existing / out of scope, agreed with the reviewer): the RuleAdapter OR_WHEN-is-an-OR collapse (AND(OR(leaves)) → AND(all leaves), needs an Epic-3 DNF-model fix), and the latent whole-array try? on locations/segments/features (same fail-open class as the audiences fix). The platform-layer ConvertSwiftSDKTests has a pre-existing native-macOS swift test concurrency flake unrelated to this change; CI validates that layer via xcodebuild on the iOS Simulator.

Non-goals honored

No bucketed_into_variation_key, no exclusion groups/layers, no UI, no backend registration, no OpenAPI, no bucketing/storage-format/event-payload change, no ruleNegation-knob-type change.

🤖 Generated with Claude Code

abbaseya and others added 11 commits July 16, 2026 00:35
…tests (RED)

Beads: ai-driven-product-dev-02i3
Agent: sdk-ios
5 failing tests (swift-testing): sibling audiences must survive a single unknown-
discriminator leaf (regression lock for the project-wide fail-open), bad audience
preserved, happy-path parity + sentinel-infra evidence pass. All failures trace to the
whole-array try? nulling audiences (ProjectConfig.swift:155-158).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…implementation (GREEN)

Beads: ai-driven-product-dev-02i3
Agent: sdk-ios
Per-audience degrading decode (ProjectConfig+AudienceDecoding.swift) mirrors the
DegradingExperience never-throws loop-termination invariant; a bad/unknown audience
degrades ALONE (siblings survive, array never nulled), preserved as a placeholder
ConfigAudience (id/key/name recovered from captured JSON) with the failed rule sub-tree
sentinel-captured in a new degradedAudienceSentinels: [String: JSONValue] map keyed by id
for IOS-2. NO generated code touched. Fixes the project-wide fail-OPEN regression.
5 targeted + 204 core tests pass; swiftlint --strict clean (incl. test-file line_length fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-v18s
Agent: sdk-ios
3 swift-testing files pinning the M2-unit contract (fail-to-compile on absent symbols
StatefulRuleTarget / RuleCondition.statefulTarget / RuleManager resolver param /
RuleAdapter.flatten(JSONValue)): AC1 8-row fixture (table-driven, id-keyed), AC8 warn-on-
unknown-target-only, AC6-unit intra-audience ALL/ANY, AC7 typed-vs-JSON generic regression,
RuleAdapter JSON-sentinel flatten. Decision-log updated (7 entries).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-v18s
Agent: sdk-ios
StatefulRuleTarget{ruleType,targetExperienceKey} + RuleCondition.statefulTarget (defaulted);
RuleManager.evaluate gains defaulted three-state resolver ((String)->Bool?) — stateful leaf
bypasses Comparisons, warn-on-unknown-target (AC8), negation in-branch; generic path
bit-identical (AC7). RuleAdapter+JSONSentinelFlatten flattens a rule tree from the IOS-1
captured sentinel JSON, reusing the shared make() builder (no forked OR/AND/OR_WHEN semantics).
NO generated code touched. 8 new + 212 core tests pass (swift test); swiftlint --strict clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tests (RED)

Beads: ai-driven-product-dev-jmr4
Agent: sdk-ios
7 Core-level end-to-end tests via public selectVariation (no new symbols). 4 discriminating
failures: fresh visitor must bucket into exp-b (today's fail-closed degrade wrongly returns
nil), AC6 ALL/ANY intra-audience combo, AC8 unknown-target warning. 3 pass today as GREEN
regression pins (bucketed-into-A excluded, AC5 read-only, AC3 relaunch — coincide with today's
unconditional degrade-fail-closed). Full core suite 219 tests, only these 4 issues. swiftlint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…implementation (GREEN)

Beads: ai-driven-product-dev-jmr4
Agent: sdk-ios
audiencePasses now async: pre-fetches DecisionStore.bucketingDecisions once (pure read, AC5),
builds a sync (String)->Bool? resolver (config.fullExperience(forKey:) for known/unknown +
String(target.id) membership in the id-keyed snapshot), dispatches degraded audiences via
ExperienceManager+MutualExclusion.flattenDegradedAudienceRules (RuleAdapter.flatten(JSONValue))
vs the byte-identical typed path. Public selectVariation signature unchanged. 7/7 IOS-3 +
219 core tests pass; swift build clean; swiftlint --strict clean. NO generated code / bucketing /
storage / event-payload change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nel flatten path

Beads: ai-driven-product-dev-v18s
Agent: sdk-ios
Decision-audit finding 2: add a generic_text_key_value vector (key:browser, contains:chrome)
to the typed-vs-JSON RuleAdapter.flatten equivalence test, locking the key-off-explicit-key
routing for the fullstack-served key-value family on the new degraded-audience JSON path.
Test-only; 4 cases pass; 219 core tests pass; swiftlint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…est (RED)

Beads: ai-driven-product-dev-y1qd
Agent: sdk-ios
Review R1: add a 5th typed-vs-JSON bit-identity vector for bucketed_into_experience — an
UNMAPPED family the typed path degrades (matchType ""/negation false) but the JSON path
currently evaluates live (equals/negated:true). Assertion FAILS, proving the divergence;
4 covered-family cases still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mplementation (GREEN)

Beads: ai-driven-product-dev-y1qd
Agent: sdk-ios
Review R1 fix: condition(fromSentinelLeaf:) now guards make() behind a namedFamilyRuleTypes
allowlist mirroring the typed condition(fromAudienceLeaf:) switch; any unmapped rule_type
(e.g. bucketed_into_experience) returns degraded() => fails closed IDENTICALLY on both paths,
never wrong-positive (AC7 + fail-closed invariant restored). 5-case bit-identity suite green,
219 core tests green, swiftlint clean. NO generated code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctor (review R2)

Beads: ai-driven-product-dev-t2mz
Agent: sdk-ios
Comment-only: the bucketed_into_experience vector comment described pre-fix DIVERGENCE; now
states the family is unmapped on BOTH paths (typed default: degraded() + JSON namedFamilyRuleTypes
guard) and the vector locks that CONVERGENCE against re-introducing the R1 over-cover. No code,
assertion, or fixture change. 5 cases pass; swiftlint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@abbaseya abbaseya self-assigned this Jul 16, 2026
@abbaseya
abbaseya requested a review from a team July 16, 2026 00:11
…rs (CI swiftlint type_name)

Beads: ai-driven-product-dev-zql2
Agent: sdk-ios
CI 'Build, Lint & Test' failed: repo-wide swiftlint --strict flagged type_name (42>40) on
MutualExclusionRuleAdapterJSONFlattenTests. Renamed type -> MutualExclusionJSONFlattenTests (31)
+ file renamed (git mv); stale doc-comment refs in 2 siblings updated. Repo-wide swiftlint clean
(0/179), build clean, 15 MutualExclusion tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

Reviewed the full diff plus the surfaces it touches (decode seam, rule engine, EM integration), cross-checked against the qs-04 spec of record, the JS SDK's feat/mutual-exclusion-rule branch, the generated schema, and the backend's OpenAPI + real config fixtures.

The fail-open fix is real and worth the PR on its own. I verified the claim rather than taking it: on feat/fullstack-v12, audiences decoded through a whole-array try?, and audiencePasses returns true on an empty resolved set — so a single unknown rule_type discriminator really did serve every audience-gated experience to everyone. Correctly fixed, and the per-element degrade + placeholder reconstruction keeps audience(id:) resolving. Worth noting this also makes the spec's Compatibility story true on iOS: it claims old SDKs fail closed on the new rule type, which was the opposite of the truth here.

Also verified and holding up:

  • The 8-row resolution fixture matches the spec's normative table row-for-row, including unknown-target → warn + vacuous-true under negation.
  • AC5 read-only: bucketingDecisions(forStoreKey:) is store[storeKey]?.bucketing ?? [:] — no LRU touch, no write, and untouched by this PR.
  • The namedFamilyRuleTypes allowlist genuinely mirrors the typed switch (22 text + country + 6 numeric + 3 bool + 4 singletons, plus the 3 key-value families) — checked case by case.
  • The loop-termination invariant holds: SentinelWrapped.init's uncaught try JSONValue(from:) can't throw, because JSONDecoder only ever hands it well-formed nodes. The comment's caveat is accurate.

Findings

1. AC6 asserts rule semantics that contradict both the generated schema and the JS SDK. (inline on MutualExclusionFixtures.swift / MutualExclusionExperienceManagerTests.swift)

OR_WHEN is an OR. The generated RuleObject doc says so — "For this OR_WHEN block to be true, any of its individual RuleElement conditions must evaluate to true" — and JS implements exactly that (_processORWHEN returns on the first matching leaf; _processAND requires all OR_WHEN blocks). The allOfRulesJSON fixture puts both leaves in one OR_WHEN block and then asserts AND behavior. The fixture fix is one line and makes the test both pass and be correct.

The underlying collapse in RuleAdapter.flatten is pre-existing (present on main, untouched here) and affects production configs today, independent of qs-04 — e.g. test-config-spa-visual.json carries OR_WHEN: [url contains "statistics", url contains "pricing", url contains "events"], which this collapse turns into "contains all three at once" → effectively never matches. Multi-leaf OR_WHEN blocks appear in ~38 backend config fixtures. I'd file that separately rather than expand this PR: the flat two-level [RuleGroup] DNF model can't represent OR(AND(OR)) without distributing, so the real fix is an Epic-3-level change.

2. Composition semantics — align with the JS SDK, which is the agreed reference. (inline on ExperienceManager.swift)

Correcting myself: an earlier revision of this review said the spec's contract is leaf-level and that "JS is the side that should move." That was wrong, and I'm retracting it. The JS spec explicitly frees the seam — "Implementation seam is free (extend literal-mode resolution for this rule_type, wrap data in a provider, or pre-resolve — implementer's choice)" — which sanctions exactly what JS does. And the "identical ALL/ANY combination" clause I leaned on means matching_options.audiences, not the rule tree: the spec's Verified Facts cite matching_options.audiences supports ALL/ANY, AC2 says "a transient audience (matching_options: ALL)", and Compatibility says "Under ANY-combination with another matching audience". All audience-level.

convertcom/javascript-sdk#416 is the reference implementation to follow. Its model:

  • A dedicated exclusion audience carrying exactly one bucketed_into_experience_key rule (documented assumption: at most one per audience) — not an exclusion leaf mixed with generic leaves in one tree.
  • Composition happens between audiences, via matching_options.audiences: ALL/ANY.
  • Resolution is pre-resolved at the data layer before the tree reaches the rule engine, leaving the rule engine byte-unchanged (AC7 by construction).

Against that reference this PR diverges on two axes, and finding 3 is the blocker for the first:

  1. AC6 is tested on the wrong axis. The mixed-tree fixtures (allOfRulesJSON/anyOfRulesJSON, one audience with two leaves) exercise a shape the reference model never produces. AC6 should be two audiences — an exclusion audience plus a generic one — asserted under matching_options: ALL and ANY, as JS #416 does.
  2. That test can't be written here yet, because matching_options.audiences isn't implemented (finding 3). Landing finding 3 is the prerequisite for matching the reference.

The in-place leaf evaluation itself doesn't need to change — under the dedicated-single-rule-audience contract, in-place and pre-resolve produce identical results. It's the tested shape and the missing matching_options support that need to move.

3. matching_options.audiences: ALL is ignored — the spec's recommended safe configuration is a no-op.

The spec's Compatibility section says: "Under ANY with another matching audience it runs without exclusion. The guide must recommend ALL and state the minimum SDK version." So the safety story depends on ALL working.

audiencePasses hardcodes a flat OR across attached audiences and never reads matching_options — documented as deferred (bd-d4p: "The per-audience ALL/ANY matching_options semantics are DEFERRED"). The field is real and served (ConfigExperienceSettings.matching_options.audiences, values all/any), and JS honors it (packages/data/src/data-manager.ts:418-421).

Concretely: a customer follows the guide, attaches the exclusion audience alongside a targeting audience under ALL; iOS ORs them; the targeting audience matches, so the visitor is eligible even though the exclusion audience returned false — both experiments run and the data is contaminated. That's the failure the spec attributes only to old SDKs. AC2 passes only because its exclusion audience is the experience's sole audience, where ANY ≡ ALL.

This is now the blocker for aligning with the reference (finding 2): JS #416's whole model rests on a dedicated exclusion audience combined with others via matching_options.audiences, and iOS cannot express that until this is honored. bd-d4p predates this PR so it isn't strictly this PR's to fix — but it can no longer be deferred silently, because the guide is about to recommend exactly the configuration iOS ignores.

Traceability

The spec of record is ai-driven-product-dev#86_bmad-output/implementation-artifacts/2026-06-09-convert-ios-sdk/**qs-04**-mutual-exclusion-rule.md. The PR body and the test-file headers cite _bmad-output/planning-artifacts/2026-06-09-convert-ios-sdk/qs-03-mutual-exclusion-rule.md, which is off by both folder and number — worth fixing the references so the next reader can find it.

One spec-side nit while it's still unmerged: the algorithm is labelled "normative — identical across all six SDKs" but hard-codes target.id.toString(), which is the JS SDK's storage shape rather than a cross-SDK invariant. Wording it as "the SDK's native bucketing-map key shape" would stop SDKs with different storage from being structurally non-compliant while behaving correctly.

Minor

  • locations / segments / features still decode through a whole-array try? — the same fail-open class fixed here for audiences, latent until a new rule type lands in a location tree.
  • RuleElementAudienceOrSentinel is declared in PolymorphicSentinels.swift but never used. Leaf-level sentinelling would drop the hand-synced allowlist entirely — it'd need a hand-authored degrading RuleObjectAudience decode so possibly not cheaper, but worth a look given the allowlist has to track the typed switch by hand forever.

Leaving as a comment rather than blocking: the substantive ask is the one-line fixture fix in finding 1; findings 2 and 3 are platform-level calls that aren't mine to make.

Comment thread Tests/ConvertSwiftSDKCoreTests/Support/MutualExclusionFixtures.swift Outdated
Comment thread Sources/ConvertSwiftSDKCore/Experience/ExperienceManager.swift Outdated

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

Superseded — folded into the review above.

abbaseya and others added 2 commits July 16, 2026 16:26
Beads: ai-driven-product-dev-z0jn
Agent: sdk-ios
Rework to whole-audience-override + matching_options ALL/ANY (JS data-manager.ts parity).
AC1 8 rows FROZEN, re-pointed to new BucketingExclusion.resolve seam (compile-RED). AC6 reworked
to TWO-audience shape: ALL fails today (current code ignores matching_options, OR-concatenates);
ANY passes as a non-discriminating pin. Removed mixed-tree allOfRulesJSON/anyOfRulesJSON + the two
AC6-unit RuleManager tests. AC2/AC3/AC5/AC8 re-pointed to singleLeafRulesJSON; AC7 stays green.
experienceJSON gains optional matching_options emit. Citation qs-03 planning -> qs-04
implementation (#86) across all test headers (0 hits remain). Supersedes the earlier
user-confirmed 'AC6 intra-audience only' scope per this session's explicit directive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tation (GREEN)

Beads: ai-driven-product-dev-z0jn
Agent: sdk-ios
Whole-audience-override exclusion + matching_options ALL/ANY (JS data-manager.ts parity):
- BucketingExclusion.swift (new): resolve(targetExperienceKey:negated:resolver:logger:) mirrors
  _resolveBucketingExclusion (target by KEY, id-keyed membership, unknown->warn+false, negation last).
- ExperienceManager.audiencePasses reworked: per-audience match (exclusion audience via whole-audience
  override detecting the statefulTarget leaf from the degraded sentinel; generic audience via typed
  RuleAdapter.flatten + RuleManager.evaluate), composed by full.settings.matching_options.audiences
  (.all=>all matched; .any/nil=>any); empty=>true. Replaces the old flat outer-OR that ignored the field.
- RuleManager.evaluate DROPS resolvingBucketedIntoExperienceKey param + stateful branch -> generic-only
  (AC7 bit-identical); header reworded to not overclaim rule-manager.ts parity, cites OR_WHEN issue iefd.
- namedFamilyRuleTypes KEPT (still on the detection flatten path + AC7 lock). M1 degrading decode preserved.
- Fixed a genuine AC6:ALL test bug (missing exp-a bucketing setup; verified independent of prod via git stash).
AC1 8 rows frozen (now via BucketingExclusion.resolve). 13 MutualExclusion + 217 core pass; repo-wide
swiftlint --strict clean (180 files); swift build clean. NO generated code touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@abbaseya

Copy link
Copy Markdown
Collaborator Author

Review addressed — re-architected to full JS #416 parity (head 827836f)

Thanks @JosephSamirL — this was a sharp review. Rather than take the one-line fixture fix, I folded the platform-level findings in and aligned iOS to the javascript-sdk#416 reference model. Summary of what changed:

Finding 3 — matching_options.audiences: ALL was a no-op (the spec's recommended safety config). Now implemented at the experience level (ExperienceManager.swift:371, mirroring data-manager.ts:418-428): ALL ⇒ every attached audience must match, ANY/absent ⇒ any, empty ⇒ unrestricted. The guide's ALL recommendation is now real on iOS.

Findings 1 & 2 — AC6 tested on the wrong axis. The mixed-single-audience fixtures are removed. AC6 is now the two-audience shape (dedicated exclusion audience + generic audience) driven by matching_options ALL/ANY, per #416.

Composition — whole-audience override. An audience with a bucketed_into_experience_key leaf resolves as a whole (BucketingExclusion.resolve), siblings not evaluated — mirrors _isBucketingExclusionRule + _resolveBucketingExclusion. In-tree leaf compose removed; RuleManager.evaluate is generic-only, AC7 bit-identical, Generated/ byte-unchanged. The 8-row leaf fixture is frozen verbatim.

Traceability. Every test header and the PR body moved from the deleted qs-03 planning path to the spec of record: _bmad-output/implementation-artifacts/2026-06-09-convert-ios-sdk/qs-04-mutual-exclusion-rule.md (ai-driven-product-dev#86).

Filed separately (agreed pre-existing / out of scope):

  • ai-driven-product-dev-iefd — the RuleAdapter.flatten OR_WHEN-is-an-OR collapse (AND(OR(leaves)) → AND(all leaves), ~38 backend fixtures; DNF-model fix). The RuleManager header now scopes its parity claim to single-leaf OR_WHEN and cites this.
  • ai-driven-product-dev-rtq2 — the latent whole-array try? fail-open on locations/segments/features (same class as the audiences fix).

Spec-side nit (not changed here): the "normative" algorithm hard-codes target.id.toString(), which is a storage shape rather than a cross-SDK invariant — flagged for a spec follow-up so SDKs with different storage aren't structurally non-compliant. Didn't touch the normative spec in this PR.

Gates: decision audit re-ran the suites (13/13 mutual-exclusion, 217/217 core) and confirmed #416 bit-faithfulness; code review clean; CI green including repo-wide swiftlint --strict.

Re-requesting your review. 🙏

@abbaseya
abbaseya requested a review from JosephSamirL July 16, 2026 14:30
@JosephSamirL
JosephSamirL merged commit 1cc3c56 into feat/fullstack-v12 Jul 16, 2026
4 checks passed
@JosephSamirL
JosephSamirL deleted the feat/mutual-exclusion branch July 16, 2026 15:14
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