Skip to content

feat: Add experimental support for flag overrides - #496

Merged
kinyoklion merged 8 commits into
mainfrom
feat/overrides
Nov 12, 2026
Merged

kinyoklion merged 8 commits into
mainfrom
feat/overrides

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

BEGIN_COMMIT_OVERRIDE
feat: Add experimental support for flag overrides, read from local JSON or YAML files or supplied by a custom override source
feat: Mark evaluations that a flag override affected with override_affected in the evaluation reason, and count them under separate summary counters
END_COMMIT_OVERRIDE

Every change in this PR was already reviewed and merged through its own PR against feat/overrides: #449, #450, #451, #452, #453, #454, #484, #493. This PR merges the branch into main and adds nothing of its own.

Flag overrides let an operator force flags and segments to known definitions on a running client, whether or not the client can reach LaunchDarkly, and remove the override when the incident is over. They are an option of the FDv2 data system (LaunchDarkly::DataSystem::ConfigBuilder#overrides), with a file-based source in LaunchDarkly::Integrations::FileData.override_source, and are experimental.

main has one commit that feat/overrides does not (#490), and the two merge without conflicts. CI on feat/overrides @ 156611b is green on every job, Windows included.

Opened from termag


Note

Overview
Adds experimental flag overrides so operators can force known flag/segment definitions at evaluation time without replacing LaunchDarkly streaming or polling.

Configuration & data path. FDv2 gains an optional override source via DataSystemConfig / ConfigBuilder#overrides. When set, the client builds an override layer, overlays it on the feature store for reads, and wires the layer’s dependency graph into the main store so prerequisite/segment changes still fan out to affected flags. Contract tests advertise flag-overrides and can wire file paths through LaunchDarkly::Integrations::FileData.override_source.

File-based source. New FileData plumbing (parse/merge, poller, reloader with in-place rewrite handling, optional listen-based watcher) backs FileData.override_source with polling or watching, duplicate-key policy, and resilient reload/retry. The listen gem is added for contract tests’ watching mode.

Evaluation & analytics. Flag/segment models carry an internal override marker. The evaluator propagates override-affected marking through prerequisites and segment matches; EvaluationReason exposes overrideAffected in JSON when true. Override-affected evaluations are excluded from individual and debug events but get separate summary counters keyed by the marker.

Client behavior. Before initialization (or when the store is unavailable), evaluations and all_flags_state can serve override entries only, with one-time warnings. Docs/examples shift to data_system_config: ... .build instead of passing raw builders.

Reviewed by Cursor Bugbot for commit 156611b. Bugbot is set up for automated code reviews on this repo. Configure here.

<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

This is the first of a series that adds flag overrides, as defined by
the OVERRIDE specification, to the Ruby server SDK. The existing FDv1
and FDv2 file data sources keep their current behavior: this PR does not
change `lib/ldclient-rb/impl/integrations/file_data_source.rb`,
`file_data_source_v2.rb`, or their specs, and the override feature is
additive throughout the series.

The file-based override source must tolerate a missing file, a file that
is being rewritten in place, and a burst of change notifications, and it
must keep the last good data when a reload fails. This change adds
`LaunchDarkly::Impl::FileData`, the file reading, parsing, merging, and
reloading code that source needs:

- Document parsing. The format comes from the file name: a name that
ends in `.yaml` or `.yml`, in any case (a file named `.yaml` included),
is YAML, and any other file, including one with no extension, is JSON.
The parser checks that the document and its `flags`, `flagValues`, and
`segments` members are objects; a JSON `null`, array or scalar is an
error.
- Whether an empty file is valid depends on the format. An empty or
white-space JSON file fails to parse (`the file has no content, which is
not a JSON document`). Empty or white-space YAML (a tab included), or
YAML that is only a comment, `---` or a null, is a document with no
entries.
- Files are read as bytes and taken as UTF-8, without a UTF-8 byte order
mark, whatever the process's default encoding. Content that is not valid
UTF-8 fails to parse in either format; a file with a UTF-16 or UTF-32
byte order mark is reported as such (`encoded in UTF-16 or UTF-32; data
files must be UTF-8`).
- JSON is parsed with `JSON.parse`, so JSON numbers are numbers (`1e3`
is `1000.0`), and content after the document, a `#` comment or a
trailing comma is an error. Comments and duplicate keys inside one
object follow the application's `json` gem: 3.x rejects both.
- An ordered merge of several documents with duplicate keys handling of
`fail` or `ignore`, expansion of each `flagValues` entry into a flag
that is on and serves the value by fallthrough (so an evaluation reports
the FALLTHROUGH reason kind, as the specification describes),
deserialization into the data model classes, and per-document entry
counts. The key under which an entry appears is its key for every
purpose: the item is stored under it, duplicates are detected by it, and
the definition's own `key` is set to it, so evaluations report the flag
by that key.
- A reloader that serializes reloads, debounces change signals, keeps
the last good result when a reload fails, retries after a bounded delay,
reports each distinct failure once, and can skip a reload whose paths,
presence and content are identical to the last applied ones. A
configured file that does not exist can be made to contribute no data.
An exception raised by the consumer's apply is treated as a failed
reload: nothing from that load is remembered, the failure is reported,
and the reload is retried. An error that a load does not expect is
logged and retried, and the worker thread survives it.
- In-place rewrites. Applications and editors truncate a file and then
write the new content, and a load in between finds the file empty. A
load that finds a file with no content (empty, white space, or YAML read
as nothing) while the data in effect has content from that file holds
the file back: it keeps the current data, logs at debug level, and arms
the retry, without reporting a failure.
- A file is held back until it has stayed so, unwritten, for one retry
delay (1 second), timed from the clock read after that file's own read.
A change of its state (modification time and size, read through any
symbolic link with the poller's helper) is a new rewrite and starts the
delay over, as does a read that finds it with content, even content that
fails to parse, or missing.
- Each file is held for at most five retry delays from its first hold;
its bound starts over only at a load that holds nothing back, whether
that load applies, is skipped, or fails.
- A load reads and classifies every file, even after one fails to read
or parse, so files emptied together are held and applied together; it
then fails with the first failure, holds files back, or merges, in that
order.
- The retry of a hold is armed for the rest of the first due file's
delay or bound, plus a tenth of the retry delay.
- A file that contributed no content to the data in effect (a
placeholder that has always been empty, a file that was missing, a file
whose emptying has been applied) is parsed at once, and so is every file
when retries are off.
- After the delay, an empty JSON file fails to parse and keeps the data;
an empty YAML file has no entries.
- A stat-based poller that compares existence, modification time, and
size, through symbolic links. A file that appears or disappears is a
change.
- A watcher that uses `rb-inotify` on Linux (a dependency of the
optional `listen` gem) to watch the directory of each file without
descending into subdirectories, and `listen` elsewhere. It picks up a
file that does not exist yet when it appears, and it watches each
directory on its own: a directory that does not exist is retried once
per second while the others stay watched, and one change is signaled
when a retry sets up a watch. Non-recursive watching matters because
`listen` scans the whole tree under a watched directory and fails when a
subdirectory cannot be read, as happens for a file in the system
temporary directory. On Linux, a watched directory that is deleted is
watched again once it exists, and one change is signaled so the reload
picks up what was written meanwhile. A configured file that is a
symbolic link is followed: the directory of the link's target is watched
for the target's name, and the links are resolved again after every
reported change, after a retry sets up watches, and when a directory
that holds a target is deleted, as a Kubernetes ConfigMap update does. A
link that cannot be resolved, for example because its target's directory
does not exist yet or was deleted, is resolved again once per second
until its target is watched, and one change is then signaled. A relative
link is expanded against the real path of the directory that holds it,
so a `..` in it is right when a directory on the way is itself a link.

A new spec, `spec/integrations/file_data_source_compatibility_spec.rb`,
pins the behaviors of the existing file data sources that their other
specs did not assert (per-file version numbering, keying by the entry's
own key, the on-plus-fallthrough expansion of `flagValues`, no retry of
a failed load, log messages, modification-time-only polling, no reaction
to a deleted file when polling, the FDv1 poller's reload cadence after a
change, and use of `listen` when it is present), so that they stay as
they are while this code exists beside them.

Not changed: YAML that is cut off mid-write can still parse, with fewer
entries or a value cut short, so the override source docs recommend
replacing a YAML file in one operation. A writer that pauses longer than
the retry delay gets the empty file applied (YAML) or reported (JSON).

## Testing

- Unit specs for the document parser (format by name, empty JSON and
YAML, UTF-8 and byte order marks, JSON numbers and trailing content),
merge, poller (including symbolic links), reloader (debounce, retry,
last-good retention, sequential reloads, stop, and the hold of an
emptied file), and watcher (including a directory with an unreadable
subdirectory, the inotify thread lifecycle, symbolic links to files in
the same and in another directory, a link that is replaced, a
ConfigMap-style update, a relative link in a directory reached through
another link, and a target directory that appears late or is recreated),
the new compatibility pins, and the unchanged existing file data source
specs.
- The reloader specs rewrite their files in place with real,
deliberately slow writes (`InPlaceWrites.rewrite` in
`spec/in_place_writes.rb`), and the hold's delays, bounds and retries
are tested on a controlled clock with synchronous loads, with a few
real-clock tests for the retry firing.
- The in-place rewrite handling was reviewed in eight rounds as #479,
with multi-agent and Bugbot reviews, and is shared with the other SDKs'
ports. On that code: a mutation check reverted each part of the fix on
its own (67 mutants); 60 fail at least one test, and the other 7 are
equivalent (the retries-off guard, the clearing of held-file records on
apply or skip, starting over every file instead of the files read, and
the path or the presence alone in the digest). The changed spec files
ran 10 times in a row on Ruby 3.2 and on JRuby 9.4 with no failures.
- Full suite and RuboCop are clean, apart from `can be automatically
loaded by Bundler.require`, which needs write access to the system gem
directory on the test machine. Each new spec was checked against a
deliberate defect in the code it covers, and each compatibility pin
against a deliberate change to the existing source it pins.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=Overrides -->
<sub>Opened from termag</sub>
<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

This PR is stacked on #449 so that the flag overrides series applies in
order. It does not use that PR's code.

Flag overrides, as defined by the OVERRIDE specification, need three
pieces below the store: a marker on the flag and segment model that says
a definition came from the override store, evaluator marking that
follows every definition an evaluation reads, and an indicator on the
evaluation reason that reports the marking to callers. This change adds
those three pieces. Nothing sets the marker yet, so the SDK behaves
exactly as before. The override store, the overlay, and the data system
wiring follow in the next PR.

The model classes `FeatureFlag` and `Segment` carry the marker as an
attribute that is never serialized, because `as_json` returns the flag
data hash. `as_override` returns a marked shallow copy and leaves the
original unchanged. Only the SDK components that manage override entries
will set it.

The evaluator marks an evaluation as override-affected when the
evaluated flag, a prerequisite flag at any depth, or a segment read
during clause matching carries the marker. A segment counts when it is
read, so a segment that does not match, or one read through a negated
clause, still marks the evaluation. The marking propagates upward only.
A prerequisite's own record reflects the definitions its own subtree
read, so a plain prerequisite inside a marked evaluation is recorded as
usual, and a prerequisite of an overridden flag is not marked by its
parent. An error result is marked when a marked definition was read
before the failure, including a failure raised while a prerequisite was
being evaluated. An evaluation that reads no marked definition keeps the
shared precomputed detail instances.

`EvaluationReason#override_affected` carries the indicator, following
the `inExperiment` and `bigSegmentsStatus` precedents. It is written to
JSON as `overrideAffected` only when true, so the wire format of
ordinary evaluations is unchanged. `with_override_affected` returns the
same instance when nothing changes, and `with_big_segments_status` keeps
the indicator.

Verification: specs for the model marker (default, copy, original
unchanged, not serialized), the reason indicator (default, JSON when
true and when false, `[]`, equality, interaction with the big segments
status), and the evaluator marking rules (direct, prerequisite at any
depth, upward-only propagation, unaffected prerequisite record, segment
read with and without a match, negated clause, nested segment,
unresolved definition, prerequisite failure, malformed override, error
raised during a prerequisite evaluation, big segments status kept). Full
suite and RuboCop are clean. Each new spec was checked against a
deliberate defect in the code it covers.

The existing FDv1 and FDv2 file data sources keep their current
behavior. This series does not change them; the override feature is
additive.

This PR also carries a spec-only fix for the Windows job, which failed
or passed depending on the random example order. Ruby 3.1 through 3.3
report file modification times in whole seconds on Windows, so a
truncation 20 ms after another one does not look like a new write. The
reloader specs that need a rewrite to look new now set the file's
modification time themselves, and the watcher spec that replaces a
symbolic link waits for the next second first on such a platform. No
code under `lib/` changes, and the specs came in with #449, so the fix
sits at the bottom of the stack.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=Overrides -->
<sub>Opened from termag</sub>
<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

This PR is stacked on #450 because it uses the override marker, the
evaluator marking, and the reason indicator that PR adds.

This change adds the override layer that the OVERRIDE specification
describes: a runtime-mutable collection of flag and segment definitions
that takes precedence over LaunchDarkly data at the store read boundary,
populated by an override source through a sink. With no override source
configured the SDK behaves exactly as before. The file-based source, the
events changes, and the contract test service follow in later PRs.

`LaunchDarkly::Interfaces::Overrides::OverrideSource` and `OverrideSink`
define how a source supplies complete snapshots to the SDK. The sink
accepts data model objects or hashes in the flag and segment data model.
The SDK marks the entries itself.

`Impl::Overrides::Layer` holds marked entries in an immutable hash that
is swapped on update, so the layer holds exactly one snapshot at any
instant and reads are lock-free. `Impl::Overrides::Overlay` sits at the
store read boundary: a read for a key returns the override entry when
one exists and the LaunchDarkly entry otherwise, an enumeration is the
union with the override entry winning, and initialization status is the
base store's alone. `Impl::Overrides::Sink` applies snapshots and
notifies flag change listeners of every flag whose merged-view
evaluation may have changed, including flags that depend on an
overridden prerequisite or segment. It computes dependency edges over
both the old and the new merged view, as the Go reference does.

`DataSystem::ConfigBuilder#overrides` accepts one override source
builder, the same `build(sdk_key, config)` protocol as the data source
builders. The FDv2 data system builds the source at construction, so an
invalid configuration raises from `LDClient.new` like other invalid
component configuration. It starts the source before its run loop, so
the initial load is in effect before the constructor returns. An
exception from the source's `start` does not prevent the client from
starting: the SDK logs it, stops the source, removes any snapshot the
source supplied, and the client runs without overrides (the SDK's own
file source does not raise from `start`; a custom source could). It
serves reads through the overlay. When the client closes, the sink stops
accepting snapshots before the source is stopped, and an exception from
`stop` is logged. An offline client builds no source and logs a warning
that the configured source is not started.

The client consults the override store before the not-initialized
short-circuit. An overridden flag is served before LaunchDarkly data
arrives, read from the override store itself, and a flag that is not
overridden still returns the client-not-ready default, even when a store
without an init marker holds LaunchDarkly data for it. `all_flags_state`
reads through the overlay. Before initialization, when the store is not
initialized and the override store holds flags, it returns a state of
those flags alone, read from the override store, with a once-per-client
warning; with no override flags the state is invalid, as it is without a
source. Without a source this state is empty, because the SDK reports no
flags before the store is initialized, so the override flags only add to
what it reports; overrides never remove a flag from the state. A failure
of the base store to list its flags propagates through the overlay, and
`all_flags_state` logs it as without a source. Without a source the
state is then invalid and empty; with override flags it holds those
flags and stays invalid, so the overrides only add to it. A client-side
SDK bootstrapped from the state uses its flags whatever `$valid` says
(js-client-sdk 3.x and the 4.x browser SDK only log a warning), so the
overridden values still reach it. A wrong-type migration result keeps
the marking of the evaluation it replaces. When the evaluator raises for
a flag that came from the override store, `variation_detail` and
`all_flags_state` report the `EXCEPTION` error reason with
`override_affected` set, from the flag's own marker. This also fixes a
pre-existing crash: `all_flags_state` raised `NoMethodError` when an
evaluation raised, because the failed flag has no evaluation state; it
now reports that flag with no prerequisites. The marker is read only
from the SDK's model objects: a custom store can return a flag as a
plain hash, as the `FeatureStore` interface documents, and such a flag
never came from the override store, so its failed evaluation still
returns the unmarked `EXCEPTION` result instead of raising.

A value-only override expands, through the file loading code from #449,
into a flag that is on and serves its single value by fallthrough, which
reports the FALLTHROUGH reason kind as the specification describes.

The OVERRIDE specification's test vectors
(`spec/fixtures/override-vectors/vectors.json`) run as a spec through
the full client stack, checking value, variation index, and reason under
the vectors' comparison rules. The vectors' per-evaluation summary
marker is asserted once the event processor carries the marking, in the
events PR.

LaunchDarkly changes reach override dependents. `Layer` keeps a
dependency graph of its entries in each snapshot, and `Store` expands
every changed LaunchDarkly item through both its own graph and the
layer's, to a fixpoint. A change to a prerequisite flag or a segment
that an override entry references therefore notifies that entry's flag,
the same way dependent flags are notified for LaunchDarkly data, and a
chain may cross between LaunchDarkly data and overrides any number of
times.

The FDv2 store's defect that seeded its dependency fan-out with symbol
keys, while `Impl::DependencyTracker` indexes dependents under string
keys, is fixed on `main` by #470, which the
stack's base now includes. The fan-out helper here also compares keys as
strings, which the fixpoint needs in any case, and the store spec keeps
the #470 specs beside this PR's specs for the override layer.

Verification: specs for the layer (marking, replacement, snapshot
atomicity under concurrent writers), the overlay (precedence, union,
deleted base items, base failure, initialization), the sink (added,
removed, changed, and unchanged entries, dependency fan-out through
prerequisites and nested segments, both merged views, no-listener path,
base failure, serialized updates), the FDv2 wiring (construction, start
order, overlay reads, stop, offline, availability unaffected), the
client (not-ready gate, all-flags state, change notifications through
the flag tracker, migration marker, lifecycle), and the specification
vectors. Full suite and RuboCop are clean, and the full suite passes on
JRuby 9.4 (the specs wait on queues without `Queue#pop`'s timeout, which
Ruby 3.1 and JRuby 9.4 lack). Each new spec was checked against a
deliberate defect in the code it covers.

The existing FDv1 and FDv2 file data sources keep their current
behavior. This series does not change them; the override feature is
additive.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **feat: Add the override store, overlay, and data system wiring**
> 
> Adds experimental, runtime flag/segment overrides:
`DataSystem::ConfigBuilder#overrides` accepts an `OverrideSource`
builder, FDv2 builds `Layer` + `Overlay` + `Sink`, wires `Store` to
cross-graph dependency notifications, and extends evaluator/reason
plumbing so prerequisites, segments, and client APIs respect override
markers while still documenting that the surface is experimental.
> 
> **Highlights**
> 
> - New override layer modules (`Impl::Overrides::{Layer,Overlay,Sink}`)
plus `Interfaces::Overrides` contracts mark model objects, merge
overrides ahead of LD data, and fan-out flag-change listeners using
merged dependency graphs.
> - `EvaluationReason`, evaluator state, prerequisite records,
migrations, and `LDClient` paths (`variation_detail`, `all_flags_state`)
now propagate `overrideAffected`, handle uninitialized stores by
consulting pure override snapshots, and keep JSON compatibility by
emitting the bit only when true.
> - FDv2 data system starts/stops override sources safely (offline skip,
resilient `start`/`stop`), exposes `override_items`, and coordinates
with `Store` via external dependency graphs; specs/vectors cover layer
semantics, notifier behavior, client flows, and file-data reloader
timing fixes on coarse clocks.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1005ed0. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

The all-flags report keeps an override-affected flag's `trackEvents`,
`trackReason`, and `debugEventsUntilDate` in this PR; #452 turns them
off (OVERRIDE Requirement 1.4.1.1) together with the rest of the event
policy for override-affected evaluations.
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=Overrides -->
<sub>Opened from termag</sub>
…ide-affected evaluations (#452)

<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

This PR is stacked on #451 because it uses the override store and the
marking that PR wires into the client.

This change carries the override-affected marking from evaluation into
analytics events, as the OVERRIDE specification requires. Event handling
keys on the marking scalar that the client hands to the event processor,
not on the evaluation reason.

`EvalEvent` and `record_eval_event` gain an `override_affected` flag.
The client sets it from the evaluation result for the evaluated flag,
from each prerequisite record's own marking for prerequisite
evaluations, and from the flag's own marker when an evaluation raises.
An unknown flag is never marked.

The event dispatcher produces no individual feature event and no debug
event for a marked evaluation, whatever the flag's configuration
requests. Marked evaluations appear in summary events only. The
summarizer keys counters by version, variation, and the marker, so
override-affected and other evaluations of the same flag, variation, and
version accumulate into separate counters. The summary event writes
`overrideAffected: true` on the marked counters only, following the
existing `unknown` marker. Event ingestion must accept that property on
summary counters before release.

`all_flags_state` presents an override-affected flag with `trackEvents`
false, `trackReason` false, and no `debugEventsUntilDate`, so a consumer
bootstrapped from the state sends no individual events for it. The flag,
its value, its version, and its reason stay in the state. When an
evaluation raises, there is no evaluation result, so the flag's own
override marker decides, as it does for the reason and for the event of
a single evaluation.

The specification vectors now also check the summary marker the client
hands to the event processor for each evaluation.

Verification: processor specs (no feature event, no debug event, marked
counter, counter split, unmarked counters unchanged, marked prerequisite
record), summarizer spec (split by marker), client specs (record
arguments for direct, segment, prerequisite, unknown, and error cases;
all-flags tracking fields including experiment tracking and
details-only-for-tracked), and end-to-end payload specs through the real
event processor (index plus marked summary only for an overridden flag,
individual events for an unaffected prerequisite inside a marked
evaluation, ordinary events when nothing is overridden). Full suite and
RuboCop are clean. Each new spec was checked against a deliberate defect
in the code it covers.

The existing FDv1 and FDv2 file data sources keep their current
behavior. This series does not change them; the override feature is
additive.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Analytics now threads an **`override_affected`** marker from
evaluations into `EvalEvent` / `record_eval_event`. The client sets it
from the evaluation result (and per-prerequisite marking for prereq
records; flag-store marker on errors).
> 
> **Event pipeline:** Override-affected evals still hit summary counters
and index events, but **no feature or debug events** are emitted even
when tracking or debug mode is on. The summarizer buckets counts by
version, variation, and the marker; summary payloads add
**`overrideAffected: true`** only on marked counters.
> 
> **`all_flags_state`:** For override-affected flags, **`trackEvents`**,
**`trackReason`**, and **`debugEventsUntilDate`** are cleared so
bootstrapped clients do not send individual events; values/reasons stay
present.
> 
> Specs cover processor, summarizer, client wiring, OVERRIDE vectors
(`summaryOverrideAffected`), and end-to-end payloads, plus
**`RecordingEventProcessor`** for tests.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e2e01d8. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=projects -->
<sub>Opened from termag</sub>
<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

This PR is stacked on #452 because it supplies the override store that
the earlier PRs add, and its client-level spec relies on the marked
reasons and events they produce.

This change adds `LaunchDarkly::Integrations::FileData.override_source`,
the file-based override source that the OVERRIDE specification defines.
It is configured with
`LaunchDarkly::DataSystem::ConfigBuilder#overrides`:

```ruby
overrides = LaunchDarkly::Integrations::FileData.override_source(paths: ["/etc/launchdarkly/overrides.json"])
config = LaunchDarkly::Config.new(data_system_config: LaunchDarkly::DataSystem.default.overrides(overrides).build)
```

The source reads one or more JSON or YAML files in the file data source
document format (`flags`, `flagValues`, `segments`) and supplies
complete snapshots to the SDK's override store. Its options are `paths`
(required, non-empty strings in precedence order; a path listed more
than once is logged and the repeats are ignored; on Windows, where file
names ignore case, paths that differ only in case count as the same
path), `duplicate_keys_handling` (`:fail`, the default, or `:ignore`,
which keeps the first file's entry), `change_detection` (`:polling`, the
default, or `:watching` through the optional `listen` gem), and
`poll_interval` (default 1 second, raised to the 1 second minimum with a
warning). Invalid options raise `ArgumentError` when the client is
created, the same way other invalid component configuration is reported.
Unlike the Go reference, an unrecognized `duplicate_keys_handling` value
and an unknown option key are rejected rather than defaulted, because
the specification treats invalid configuration as a construction error
and an emergency mechanism should fail loudly.

Behavior comes from the shared file data code added in #449:
- The format comes from each file's name, as sdk-specs #272 recommends:
a name that ends in `.yaml` or `.yml`, in any case, is YAML, and any
other file, including one with no extension, is JSON. The existing file
data sources read every file with the YAML parser, which also reads most
JSON; the override source does not, so a YAML document in a `.json`
file, or JSON with a `#` comment or a trailing comma, does not load.
Files must be UTF-8; a UTF-16 file, as Windows PowerShell 5.1 writes
with `>`, fails to load with a clear message and keeps the overrides in
effect.
- An empty JSON file is not a valid document: it fails to load and the
overrides in effect stay. To remove a file's overrides, delete the file
or write `{}`.
- Override files can be rewritten in place. When the source finds a file
whose overrides are in effect empty (or, for YAML, with only a comment
or `---`), it holds the file back and keeps the overrides in effect
until the new content is written: until the file has stayed so,
unwritten, for one retry delay (1 second), and for at most five retry
delays while it keeps being rewritten. Changes to the other files wait
with it, since a reload applies all files or none. The mechanism is
described in #449; the public docs say only what an application needs.
- A configured file that does not exist contributes no overrides, so a
file can be created later and deleting a file removes its overrides; a
file that exists but cannot be read or parsed fails that reload and
keeps the last good overrides, with a logged failure and a retry; a
rewrite with identical content is not reapplied; the initial load
completes before the client constructor returns.

A `flagValues` entry expands into a flag that is on and serves the value
by fallthrough, so the evaluation reports the FALLTHROUGH reason kind.
Every applied change is logged at Info with the overrides in effect and
what each file supplied, and the reloader's log lines for this source,
including the hold's debug lines, carry a `FileOverrideSource` prefix.

The `override_source` docs say how the format is chosen, that files must
be UTF-8, that comments and repeated keys in JSON depend on the
application's `json` gem, that files can be rewritten in place, how to
remove a file's overrides, that an empty JSON file is not a valid
document, and that replacing a YAML file in one operation avoids briefly
applying a partly written file.

## Testing

- Specs for the builder (defaults, absolute paths, every invalid option,
`listen` requirement, poll interval clamping), the initial load (merge,
fallthrough-form expansion, YAML, missing file, all files missing,
malformed file, duplicate keys under both handlings), the Info log
format, polling change detection (change, appearance, deletion,
malformed edit with recovery, identical rewrite, stop, stop before
start), watching change detection (change, appearance, deletion), and a
client-level check of serving and reloading overrides through
`LDClient`.
- The specs rewrite override files in place with real, deliberately slow
writes. End-to-end specs check that the source never supplies a snapshot
without the override, and that a client never serves the LaunchDarkly
value, while a JSON or YAML file is rewritten in place with a 300 ms
pause, with polling and watching change detection; also for a YAML file
whose leading comment is written before the pause, for five rewrites
about a second apart with the default timing, for a JSON file and a YAML
file that stay empty, and for files that are UTF-16 or not valid UTF-8,
at startup and at runtime.
- The in-place rewrite handling was reviewed in eight rounds as #479
(multi-agent and Bugbot reviews); see #449 for the mutation check (67
mutants, 60 killed, 7 equivalent) and the loops (10 runs in a row on
Ruby 3.2 and on JRuby 9.4, no failures).
- The `flag overrides` suite of sdk-test-harness (3.2.0-alpha.13, built
from #515, whose override files are rewritten in place and have no
extension) passes against the test service of #454: 79 tests ran, all
passed.
- Full suite and RuboCop are clean, apart from `can be automatically
loaded by Bundler.require`, which needs write access to the system gem
directory on the test machine. Each new spec was checked against a
deliberate defect in the code it covers.

The existing FDv1 and FDv2 file data sources keep their current
behavior. This series does not change them; the override feature is
additive.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=Overrides -->
<sub>Opened from termag</sub>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Adds **`LaunchDarkly::Integrations::FileData.override_source`** so
operators can load **flag and segment overrides from local JSON/YAML
files** that take precedence over LaunchDarkly at evaluation time, wired
through **`DataSystem::ConfigBuilder#overrides`**. The new
**`FileOverrideSource`** reuses the shared **`FileData::Reloader`**
(with **`skip_missing_paths`**, **`skip_unchanged`**, polling or
optional `listen` watching) and logs applied overrides with a
**`FileOverrideSource:`** prefix.
> 
> **`Reloader`** gains a configurable **`log_prefix`** (default
`[LDClient]`) and logs unexpected errors directly instead of
**`Util.log_exception`**.
> 
> Documentation examples for custom data systems now use
**`data_system_config:`** and **`.build`** on **`DataSystem`** builders
instead of the old **`data_system:`** shape.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
46b384f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## Summary

This PR is stacked on #453 because the test service configures the
file-based override source that PR adds.

The contract test service now advertises the `flag-overrides` capability
and accepts the `overrides` block of the SDK configuration described in
the test harness service specification. `filePaths`,
`duplicateKeysHandling`, `changeDetection`, and `pollIntervalMs` map to
the options of `LaunchDarkly::Integrations::FileData.override_source`,
which is set on the FDv2 data system builder. An `overrides` block
without a `dataSystem` block is an error, as in the Go reference
service.

The `listen` gem joins the contract test bundle so that the harness's
watching change detection tests can run. The service port can be set
with the `PORT` environment variable and still defaults to 9000, so the
existing Makefile and CI steps are unchanged.

Verification against the prebuilt harness (sdk-test-harness branch
rlamb/sdk-2657/flag-overrides-tests at 9ad082c, `flag-overrides`
capability): `-run 'flag overrides'` ran 38 tests with 0 failures, and
the full suite with no filter ran 4733 tests with 0 failures (13 skipped
by pre-existing capability and long-running gates). The first run of the
watching mode tests failed because the watcher relied on `listen`, which
could not scan the system temporary directory where the harness writes
its files; the watcher change in #449 corrects that, and both runs above
were made with it in place.

The existing FDv1 and FDv2 file data sources keep their current
behavior. This series does not change them; the override feature is
additive.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Enables the Ruby contract test client to wire the FDv2 `overrides`
block to `LaunchDarkly::Integrations::FileData.override_source`, mapping
harness fields to paths, duplicate-keys handling, change-detection mode,
and optional polling interval, and rejects overrides when no
`dataSystem` is configured to match harness expectations.
> 
> Advertises the `flag-overrides` capability from the test service,
allows the bind port to be overridden via `PORT` (default 9000), and
adds the `listen` gem to support watching change detection for
file-based overrides.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
bb7a5ca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…nd the key (#493)

<!-- TERMAG_PR_BODY_BEGIN -->
**Requirements**

- [x] I have added test coverage for new or changed functionality
- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
- [ ] I have validated my changes against all supported platform
versions (run on Ruby 3.2 only)

**Related issues**

- Spec: launchdarkly/sdk-specs#288 (Requirements 1.1.1.1, 1.2.8.1 and
1.3.6.2: the override layer never holds a deleted item).
- [SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)

This PR is stacked on #454 so that the PRs below it do not change. Its
base is `rlamb/overrides-ruby-contract-tests`.

**Describe the solution you've provided**

Before:

- The override sink already rejected a snapshot with a deleted flag or
segment with `ArgumentError`, before the layer changed. Nothing logged
the rejection when a custom source supplied the snapshot after it had
started. The message used the internal kind namespace: `the override
features entry f is deleted`.
- The file override source failed the reload only through that sink
check. The log named neither the file nor the kind as users know it:
`Unable to load flags: the override features entry f is deleted`.

After:

- The file override source checks each file as it reads it, before it
supplies a snapshot. A file with a flag or segment entry marked as
deleted fails the reload in the same way as a file that cannot be
parsed: the previous overrides stay, the existing retry applies, and the
failure is logged at Error level with the file and the key, for example
`[LDClient] FileOverrideSource: Unable to load flags: flag "f" in
/path/overrides.json is marked as deleted; remove the entry to remove
its override`. The check runs through a new `check_document` option of
the shared reloader (`impl/file_data/reloader.rb`), and only the
override source sets it.
- The sink still raises `ArgumentError` for a snapshot with a deleted
flag or segment. It now also logs the rejection at Error level, with
"flag" or "segment" and the key: `[LDClient] Rejected an override
snapshot, keeping the overrides in effect: override flag "f" is marked
as deleted; leave the flag out of the snapshot to remove its override`.
The whole snapshot is rejected, the layer is unchanged, and no change
notifications are sent.
- `deleted: false` is an ordinary definition and still loads.
- Public docs: `FileData.override_source` and
`OverrideSink#set_overrides` each gain one sentence about entries marked
as deleted.

The ordinary file data sources (v1 `FileDataSourceImpl` and FDv2
`FileDataSourceV2`) are unchanged on purpose. Rejecting deleted entries
there would be a breaking change, which is planned for a later major
version. Neither of them uses the shared `impl/file_data` engine. The
new specs pin that both of them, and the reloader without a document
check, still load entries marked as deleted.

Tests added or changed:

- `spec/impl/overrides/sink_spec.rb`: a deleted flag, and a deleted
segment given as a model object, each reject the whole snapshot. The
other entries are not applied, the layer is unchanged, nothing is
notified, and the Error log names the kind and the key. Entries with
`deleted: false` are accepted. The existing test's message expectations
moved into these tests.
- `spec/integrations/file_override_source_spec.rb`: a deleted flag fails
the initial load, and the log names the file and the key. On a running
source, a deleted flag keeps the previous overrides, and the fix is
applied. A deleted segment in a YAML file keeps every file's overrides,
including a change to the other file, until the segment entry is fixed.
`deleted: false` loads. With a client, the override is still served, the
log names the file, and the sink's rejection never appears because the
source fails the load first.
- `spec/impl/file_data/reloader_spec.rb`: a document check that raises
fails the load and keeps the last good result. The other files are still
checked. Without a check, deleted entries are combined as before.
- `spec/integrations/file_data_source_v2_spec.rb`,
`spec/integrations/file_data_source_spec.rb`: pins that the data sources
still load entries marked as deleted.

How I ran them (Ruby 3.2.3):

- `bundle exec rspec spec/impl/overrides spec/impl/file_data
spec/integrations/file_override_source_spec.rb
spec/integrations/file_data_source_v2_spec.rb
spec/integrations/file_data_source_spec.rb`: 312 examples, 0 failures.
- `bundle exec rspec spec` (database store specs skip by default): 1681
examples, 1 failure. The failure is
`spec/launchdarkly-server-sdk_spec.rb:5`, the `Bundler.require` autoload
test. Its inline bundler gets `Permission denied` on
`/var/lib/gems/3.2.0/bundler.lock` on this machine, so it is a local
environment problem and unrelated to this change.
- I ran the new specs against the parent commit with only the spec files
copied. The 7 behavior tests fail there, and the 5 pins and `deleted:
false` tests pass. Two mutants also fail the expected tests: removing
the sink's log line, and skipping segments in the file check.
- `bundle exec rubocop` on the changed files: no offenses.

**Describe alternatives you've considered**

- Checking only in the sink: the sink cannot name the file. Under
retries, the reloader logs repeats of the same failure at Debug, so the
first Error line would still not say which file to fix.
- Checking inside `FileData.merge`: merge does not know the file paths,
and the ordinary data sources would share it once they move onto the
engine. A per-document hook keeps the rule on the override path.
- Logging every sink rejection, including an entry without a key: a file
with an empty key (`"": ...`) reaches the sink, and the reloader retries
every second. An Error line from the sink on each retry would repeat
what the reloader already logs once. So only the deleted-item rejection,
which the file source never reaches now, is logged by the sink.

**Additional context**

Separate follow-up, not fixed here: the v1 file data source has its own
bug. `LDClient.new` raises `NoMethodError` when a file has an entry
marked as deleted, because `dependency_tracker.rb:60` checks `deleted`
only on a hash. Line 70 then reads the prerequisites of a deleted model,
which are nil. The same happens on `main`. The v1 pin in this PR writes
to a data store without an update sink, so it does not touch that path.
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=ruby-server-sdk -->
<sub>Opened from termag</sub>


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Adds a **`check_document`** hook to `FileData::Reloader` so each
parsed file can be validated before merge; a `MergeError` from the hook
fails the reload like a parse error and keeps the last good snapshot.
> 
> The **file override source** wires this to
**`reject_deleted_entries`**, which rejects files containing flag or
segment entries with `deleted` set (other than false/null), logging
errors that name the **file path** and **key**. **Override sink** still
rejects full snapshots with deleted items but now uses user-facing
"flag"/"segment" wording and **logs an ERROR** when rejecting, so custom
sources get visibility too.
> 
> **Public docs** for `FileData.override_source` and
`OverrideSink#set_overrides` note that deleted entries fail loads / are
rejected. **v1 and FDv2 file data sources** are unchanged and still
accept deleted entries (pinned by new specs).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
cbf2a1d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…ich deployments watching does not follow (#484)

<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

Follow-up to #449 for two findings raised on the Python port of the same
watcher (launchdarkly/python-server-sdk#525), both of which apply here.

- A configured link that cannot be resolved keeps the once-per-second
retry running. When a retry resolved such a link to a target in a
directory that was already watched, for example because another
configured file is in it, it added no watch and so ran no callback.
Content that appeared while the link could not be resolved was then not
read until some later change. `refresh_links` now reports a change
whenever a link leads to a target it did not lead to before, so the
retry runs the callback once.
- The watcher comment said that a link that comes to point elsewhere is
followed. That holds when the old target's directory is deleted, as a
ConfigMap update does, but not for a link further along the chain that
is pointed elsewhere while its old target stays, a configured directory
that is itself a link and is pointed elsewhere, or a configured
directory that is renamed and replaced. The comment now names those
cases and points such deployments, like a `current` link to a release
directory, to polling, which is the default. The override source's
`change_detection` documentation says the same in #453.

## Tests

A new watcher spec configures a link that leads through a directory that
does not exist yet to a directory that another configured file keeps
watched, then creates the missing directory as a link. It fails without
the change and passes with it. The watcher specs pass (24 examples), the
full suite passes apart from `spec/launchdarkly-server-sdk_spec.rb:5`,
which fails the same way on `feat/overrides` on this box because Bundler
cannot write its lock file, and RuboCop reports no offenses.

[SDK-3249](https://launchdarkly.atlassian.net/browse/SDK-3249)
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=ruby-server-sdk -->
<sub>Opened from termag</sub>


[SDK-3249]:
https://launchdarkly.atlassian.net/browse/SDK-3249?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Fixes **file data watcher** behavior when a configured symlink finally
resolves to a file in a directory that was **already** being watched
(e.g. another configured path). `refresh_links` now treats newly
discovered link targets in existing watches as a change, so the retry
loop invokes **`on_change` once** instead of silently skipping until
some unrelated filesystem event.
> 
> Watcher docs are tightened to list symlink/deployment patterns
**inotify does not track** (mid-chain repointing, directory-as-symlink
swaps, rename-and-replace layouts) and to steer **`current` → release**
style setups toward **polling**.
> 
> Specs cover resolution into an already-watched directory and assert
the callback is **not** fired on every failed watch retry.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d232195. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@kinyoklion
kinyoklion requested a review from a team as a code owner November 12, 2026 17:17

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 156611b. Configure here.

reason: detail.reason,
prerequisites: eval_state.prerequisites,
# A failed evaluation has no evaluation state, so it reports no prerequisites.
prerequisites: eval_state&.prerequisites,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Failed flag eval reuses prior result

Medium Severity

eval_result and eval_state are assigned inside the all_flags_state loop and never cleared when evaluate raises. In Ruby those names are method-local, so a later failed flag keeps the previous flag’s result. eval_result.nil? is then false, so override_affected and prerequisites come from the earlier flag. Bootstrap JSON can list another flag’s prerequisites, and tracking fields can be wrongly enabled or suppressed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 156611b. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This reply was written by an AI agent (Claude Code) working on behalf of @kinyoklion.

This one does not apply. eval_result and eval_state are assigned only inside the features.each do |k, f| block, and nowhere earlier in all_flags_state. A local variable first assigned inside a block belongs to that block, and each iteration starts with it unset. So when evaluate raises for a flag, eval_result is nil for that flag, and override_affected and prerequisites cannot carry over from the flag before it. For example, ruby -e '[1,2].each { |i| (r, st) = [i, :s] if i == 1; p [i, r, st] }' prints [1, 1, :s] and then [2, nil, nil].

prereq_flag[:debugEventsUntilDate],
prereq_of_flag[:key],
prereq_flag[:samplingRatio],
!!prereq_flag[:excludeFromSummaries]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pre-init eval reads leftover store data

Medium Severity

Before initialization, only the top-level flag is read from the override store. evaluate still resolves prerequisites and segments through the overlay, which falls through to the base store. A store that is not initialized can still hold LaunchDarkly data, so an override flag can be decided by leftover prerequisites or segments that a direct evaluation of those keys would refuse to serve.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 156611b. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This reply was written by an AI agent (Claude Code) working on behalf of @kinyoklion.

This is the intended behavior, and @kinyoklion has accepted it. Before initialization, the SDK serves a flag that the override layer holds instead of returning the not-ready default (OVERRIDE Requirement 1.5.3). Every definition that the evaluation then reads goes through the read boundary (Requirement 1.5.1), so a prerequisite or segment that has no override comes from LaunchDarkly data, including data in a store that is not initialized yet. The Go SDK behaves the same way.

An operator who wants an override flag to depend only on overridden definitions can override its prerequisites and segments as well.

@kinyoklion
kinyoklion merged commit 9e2452c into main Nov 12, 2026
16 of 17 checks passed
@kinyoklion
kinyoklion deleted the feat/overrides branch November 12, 2026 21:17
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