Skip to content

feat: implement numeric coercion contract (ADR) #1995

Description

@toddbaert

Tracking issue for implementing the numeric coercion ADR across flagd, flagd-schemas, flagd-testbed, and all flagd providers.

Contract summary

Per docs/architecture-decisions/numeric-coercion.md (merged in #1979):

  • Numeric variants are returned through a numeric accessor only when the conversion is lossless; otherwise TYPE_MISMATCH.
  • Numeric flag values are constrained to the IEEE-754 safe-integer range, [-(2^53 - 1), 2^53 - 1], in the flag-definition schema. This is an ordinary schema validation, surfaced like any other schema validation (today flagd logs schema violations as warnings; that behavior is unchanged). Hard-failing a load on schema violations is intentionally out of scope for this work and is tracked separately as its own feature.
  • The lossless-coercion contract applies identically across gRPC, OFREP, and in-process evaluation.
  • Int bounds are relative to the accessor in use. Accessor widths differ by language; the shared testbed treats Long (int64) as the canonical wide numeric accessor and only encodes language-agnostic assertions, with int32-specific cases behind @int32-bounded.

Per-language accessor widths

Language Integer accessor Long accessor Notes
Go (flagd-core) int64 (ResolveIntValue) same (int64 is canonical) no separate Long; Integer is the canonical Long, so Go excludes @int32-bounded
Java int32 (getInteger) int64 (getLong, java-sdk#1985) Long safe-delegates to int; canonical wide accessor
.NET int32 int64 (Int64)
Python arbitrary-precision int n/a effectively wide
JS / Web double double no int64; exact up to 2^53 - 1 (the cap)

Float accessors cap at the safe-integer range.

No wire format changes. AnyFlag.double_value in ResolveAll remains as-is; the ADR intentionally treats the parsed int/float JSON type as non-authoritative, which keeps bulk eval faithful under the same rule.

Breaking change scope

  • Callers relying on silent truncation (Go, Java in-process): 3.14 via getIntegerDetails returns TYPE_MISMATCH instead of 3.
  • Some SDKs flip the other way: 10.0 via getIntegerDetails returns 10 (previously TYPE_MISMATCH in Python and .NET).
  • Config authors: flag definitions with numeric values outside [-(2^53 - 1), 2^53 - 1] fail schema validation (a load-time warning today, consistent with every other schema validation). They are not hard-rejected at load by this work.

flagd is pre-1.0, so the coercion behavior change ships as a feat!: minor bump with breaking changes called out in release notes.

Testbed rollout

There is no existing numeric-coercion suite to preserve. This work adds a new, opt-in @numeric-coercion tagged suite (following the opt-in @fractional-v1/@fractional-v2 precedent) so providers adopt it as they migrate. Coverage is added to both the provider gherkin/ suite and the self-contained evaluator/ suite. Width-specific expectations sit behind @int32-bounded; wide and boundary values use the canonical Long accessor.

Rollout order

  1. flagd-schemas: add the safe-integer range to numeric variant values (schema validation)
  2. flagd-testbed: add ADR-conformant scenarios under @numeric-coercion (both suites), release new tag
  3. flagd + flagd-core (Go): implement the lossless-coercion contract, bump testbed submodule
  4. Providers (in parallel once the testbed tag lands): bump testbed submodule, implement the lossless-coercion contract in each in-process resolver, apply the same lossless check client-side for AnyFlag.double_value
  5. Coordinated provider releases

Child issues

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions