| name | sdk-evaluator-contract |
| description | Use when changing SDK, evaluator, fallback, local evaluation, config consumption, cache, polling, refresh, offline mode, localStorage cache, or ETag behavior. |
Generated from ai/registry.json. Do not edit manually.
Canonical skill: ../../../ai/skills/sdk-evaluator-contract.md.
Referenced context:
../../../ai/glossary/config-sdk-terms.md
../../../ai/rules/sdk-evaluator-rules.md
../../../ai/architecture/sdk-evaluation-flow.md
../../../ai/examples/good-evaluator-test.md
../../../ai/examples/good-sdk-client.md
This file is compiled from canonical AI knowledge files. Edit canonical files under ai, then run npm run ai:sync.
Compiled AI Skill: sdk-evaluator-contract
Canonical Skill: ai/skills/sdk-evaluator-contract.md
SDK Evaluator Contract
Use this skill when changing packages/sdk-js, packages/evaluator, packages/react, evaluation context, local evaluation, SDK cache, polling, refresh, offline behavior, or SDK consumption of public config JSON.
Goal
Keep SDK consumption and local evaluation predictable, private, typed, and fallback-safe.
Read First
ai/glossary/config-sdk-terms.md
ai/rules/sdk-evaluator-rules.md
ai/architecture/sdk-evaluation-flow.md
ai/examples/good-evaluator-test.md
ai/examples/good-sdk-client.md
Related References
- Use
api-public-config-contract when the public Config JSON shape changes.
- Use
feature-flag-domain when flag type or rollout semantics change.
Workflow
- Identify whether the change affects public SDK API, config fetch/cache, SDK modes, polling lifecycle, evaluator semantics, React SDK behavior, or public config parsing.
- Preserve fallback behavior for network failures, missing flags, invalid config, unsupported schema versions, and type mismatches.
- Preserve fallback-safe local behavior for advanced targeting: missing attributes, invalid segments, missing prerequisites, invalid prerequisite values, and prerequisite cycles should not throw into app code.
- Preserve lazy loading as the default mode and keep
refresh()/close() behavior stable.
- Preserve ETag behavior: send
If-None-Match with cached ETags and avoid JSON parsing on 304 Not Modified.
- Preserve valid cache on refresh failure or invalid remote config.
- Preserve SDK subscription semantics: notify only when a valid changed config replaces cache, and never notify for
304, failed refreshes, invalid configs, or equivalent configs.
- Keep persistent cache opt-in and free of raw SDK keys.
- Keep evaluator logic pure and deterministic.
- Keep evaluation context local to SDK/evaluator code.
- Add behavior tests at the package boundary that changed.
Expected Output
- Public SDK shape remains stable unless explicitly changed.
- Evaluator behavior follows rules, rollout, default value, then fallback.
- Prerequisite flags are evaluated locally from the same Config JSON and never trigger network or API calls.
- SDK modes remain explicit:
lazy, auto, manual, and offline.
- Auto polling can be stopped with
client.close().
- SDK subscribers can observe cache-changing config updates without receiving evaluation context or cache internals.
- SDK and evaluator packages do not import server-only code.
- New SDK capabilities are driven by product requirements, not speculative overbuild.
Verification
- Run
npm --workspace @capture-flag/evaluator run test after evaluator changes.
- Run
npm --workspace @capture-flag/sdk-js run test after SDK changes.
- Run
npm --workspace @capture-flag/react run test after React SDK changes.
- Run package builds after public type changes.
Referenced Context
Reference: ai/glossary/config-sdk-terms.md
Config And SDK Terms
Terms shared by API, public config, evaluator, SDK, and client UI.
Feature Flag
SDK-visible setting controlled by Capture Flag. Supported types are boolean, string, integer, double, json_object, and json_array.
Default Value
Configured flag value stored in the database and emitted in Config JSON. This is not the same as SDK fallback value.
Fallback Value
Value supplied by application code when calling getValue. The SDK returns fallback when config is unavailable, invalid, missing, or type-mismatched.
Rules
Ordered targeting rules emitted as rules in public Config JSON. The evaluator checks rules before percentage rollout.
Segment
Reusable group of attribute conditions scoped to one config and emitted as segments in public Config JSON.
Segment Reference
A rule condition shaped as { "segment": "segment-key" }. It is evaluated locally by checking the referenced segment conditions against the Evaluation Context.
Prerequisite Flag
A rule condition shaped as { "prerequisiteFlag": "flag-key", "operator": "equals", "value": true }. It evaluates another flag from the same Config JSON locally before deciding whether the current rule matches.
Advanced Targeting
Targeting features beyond simple attribute equality: prerequisite flags, arrayContains, date comparisons, and SemVer comparisons.
Percentage Rollout
Deterministic distribution of values based on a rollout attribute such as identifier.
Percentage Attribute
Context attribute used for rollout bucketing. Defaults to identifier.
SDK Key
Read-only credential scoped to one config and one environment. Stored as a hash in the API database.
Raw SDK Key
The full SDK credential shown only immediately after creation. It must never be stored, logged, audited, or re-displayed.
Key Prefix
Display-safe prefix derived from the raw SDK key and stored for UI identification and audit metadata.
Config JSON
Versioned public JSON downloaded by SDKs. It contains local-evaluation data, not evaluated flag results.
React SDK
React provider and hook package that receives a JavaScript SDK client and evaluates flags through that client.
Config Environment State
Per config + environment state that stores revision, ETag, and generated timestamp for public config caching.
ETag
HTTP cache validator used by SDK clients through If-None-Match.
Lazy Loading
Default SDK mode. The client fetches config only when no cache exists or when the cached entry is older than cacheTtlMs.
Auto Polling
SDK mode where the JavaScript client refreshes config in the background on pollIntervalMs.
Manual Refresh
SDK mode where getValue uses the current cache and the application calls refresh() to fetch new config.
Offline Mode
SDK mode where the client uses only existing cache and never performs network requests.
Memory Cache
Default in-process SDK cache used by every client instance.
localStorage Cache
Browser persistent cache enabled explicitly through SDK options. It stores config, ETag, timestamp, cache schema version, and scope fingerprints, never raw SDK keys or raw base URLs.
Cache TTL
Duration used by lazy loading to decide whether a cached config should be refreshed.
Cached ETag
ETag stored with the cached config and sent on refresh through If-None-Match.
Client Close
client.close() stops SDK-owned background polling timers.
Reference: ai/rules/sdk-evaluator-rules.md
SDK Evaluator Rules
Rules for packages/sdk-js, packages/evaluator, and packages/react.
Always
- Preserve the SDK shape unless the task explicitly changes it:
createClient(options) returns getValue<TValue>(key, fallbackValue, context?), refresh(), close(), and subscribe(listener).
- Keep
fallbackValue distinct from stored config defaultValue.
- Fetch public config with the SDK key, then evaluate locally.
- Keep the evaluator pure and deterministic: no network, database, clock, or random dependencies.
- Evaluate segment reference conditions locally from public config
segments and the Evaluation Context.
- Treat missing, invalid, empty, or nested segment references as non-matches.
- Evaluate prerequisite flag conditions locally from the same public Config JSON and the same Evaluation Context.
- Treat missing prerequisite flags, invalid prerequisite values, unsupported prerequisite operators, and prerequisite cycles as non-matches.
- Keep prerequisite flag operators limited to
equals and notEquals unless the public contract is explicitly expanded.
- Support advanced attribute operators including
arrayContains, dateBefore, dateAfter, semverEquals, semverGreaterThan, semverGreaterThanOrEquals, semverLessThan, and semverLessThanOrEquals.
- Accept date comparison values only as numeric timestamps or ISO
YYYY-MM-DD/date-time strings with timezone.
- Compare strict SemVer 2.0.0 strings with
MAJOR.MINOR.PATCH, prerelease precedence, and ignored build metadata.
- Use deterministic percentage rollout with FNV-1a 32-bit over
${flagKey}:${attributeValue}, UTF-16/JavaScript code units, modulo 10000, and percentage basis points with at most two decimal places.
- Return the caller fallback for missing flags, invalid config, unsupported schema versions, type mismatches, and request failures.
- Keep lazy loading as the default SDK mode.
- Keep SDK modes explicit:
lazy, auto, manual, and offline.
- In lazy mode, respect
cacheTtlMs before refreshing.
- In auto mode, poll in the SDK client and stop background polling through
client.close().
- In manual mode, use the current cache from
getValue and fetch only through refresh().
- In offline mode, never perform network requests.
- Send
If-None-Match when a cached ETag exists and treat 304 Not Modified as a freshness update without reprocessing JSON.
- Preserve an existing valid cache when refresh fails or returns invalid config.
- Notify SDK subscribers only when a valid config with a changed identity replaces the cache.
- Do not notify SDK subscribers for
304 Not Modified, request failures, non-OK responses, invalid config, or equivalent config responses.
- Keep localStorage cache opt-in, scoped by base URL and SDK key fingerprint, and never persist raw SDK keys or raw base URLs.
- Keep options minimal and explicit.
Never
- Do not send evaluation context to the API.
- Do not allow segment evaluation to perform network calls or API lookups.
- Do not allow prerequisite evaluation to perform network calls or API lookups.
- Do not import server-only packages into SDK, evaluator, or React SDK packages.
- Do not throw SDK evaluation failures into application code when fallback behavior is possible.
- Do not add retries or custom cache adapters before product requirements call for them.
- Do not add compatibility layers for unshipped config schema versions.
Evaluation Order
- Evaluate rules in order, resolving segment and prerequisite flag conditions.
- Evaluate percentage rollout.
- Return config
defaultValue.
- Return SDK call
fallbackValue when config is unavailable, invalid, missing, or mismatched.
Reference: ai/architecture/sdk-evaluation-flow.md
SDK Evaluation Flow Architecture
SDK evaluation is local. The API only serves config data.
Package Roles
packages/evaluator: pure deterministic evaluation engine.
packages/sdk-js: fetches public config, owns cache/polling behavior, and calls the evaluator.
packages/react: React provider and useFeatureFlag hook around the JS SDK client.
SDK Flow
- App creates a client with
createClient({ baseUrl, sdkKey }).
- The default mode is
lazy.
getValue(key, fallbackValue, context) resolves a config according to the selected SDK mode.
- The SDK validates config shape before replacing cache.
- The SDK calls
evaluate from @capture-flag/evaluator.
- Request failures, invalid JSON, unsupported config, missing flags, and type mismatches return fallback.
Cache And Refresh Flow
- Memory cache is always the in-process source of truth.
localStorage cache is browser-only and opt-in through SDK options.
- Persistent cache stores config data, ETag, timestamp, cache schema version, and scope fingerprints, not raw SDK keys or raw base URLs.
- Lazy mode fetches when no cache exists or
cacheTtlMs has expired.
- Manual mode returns current cache from
getValue; applications call refresh() to fetch.
- Offline mode returns current cache only and never performs network requests.
- Auto mode reuses cache from
getValue and refreshes in the background on pollIntervalMs.
close() stops auto polling timers.
- Concurrent refresh calls share the same in-flight refresh promise.
- SDK subscriptions observe valid config changes without exposing cache internals.
HTTP Cache Flow
- The SDK includes
If-None-Match when the current cache has an ETag.
304 Not Modified updates cache freshness without parsing or replacing config JSON.
- Non-OK responses do not replace existing cache.
- Invalid config responses do not replace existing valid cache.
- Successful valid responses replace cache and persist it when localStorage is enabled.
- Subscribers are notified only when the accepted config identity changes.
- Subscribers are not notified for
304, failed refreshes, invalid configs, or equivalent config responses.
Evaluator Flow
- Validate config shape and
schemaVersion.
- Find requested flag.
- Validate stored default value type.
- Evaluate rules top-down, resolving segment reference and prerequisite flag conditions locally.
- Treat missing attributes, invalid segment references, missing prerequisites, invalid prerequisite values, and prerequisite cycles as non-matches.
- Evaluate deterministic percentage rollout.
- Return config default value.
- Return caller fallback when evaluation cannot safely produce a typed value.
Privacy Boundary
Evaluation context stays in the SDK process and is never sent to the public config API.
Reference: ai/examples/good-evaluator-test.md
Good Evaluator Test
Source: packages/evaluator/test/index.spec.ts (sha256: 40f396c39736d1e39ecc8575adc4f3566a656b3665b3513c10b64d3d49018e81)
Why this is canonical:
- Builds small config fixtures around the public evaluator contract.
- Tests observable evaluation behavior instead of internals.
- Covers fallback, rule order, segment references, prerequisite flags, advanced operators, rollout determinism, JSON values, and type mismatch behavior.
Canonical evaluator test pattern from packages/evaluator/test/index.spec.ts.
function createFlag(overrides: Partial<CaptureFlagConfigFlag> = {}): CaptureFlagConfigFlag {
return {
defaultValue: false,
percentageAttribute: "identifier",
percentageOptions: [],
rules: [],
type: "boolean",
...overrides,
};
}
function createConfig(flag: CaptureFlagConfigFlag = createFlag()): CaptureFlagConfig {
return {
configKey: "default",
environment: "production",
flags: {
newCheckout: flag,
},
generatedAt: "2026-05-12T00:00:00.000Z",
projectKey: "ecommerce",
revision: 1,
schemaVersion: 1,
};
}
Tests build small config fixtures and assert behavior through the public evaluate function.
Behavior Focus
- Missing or invalid config returns fallback.
- Rules evaluate top-down.
- Segment references evaluate locally and invalid segment references do not match.
- Prerequisite flags evaluate locally and cycles do not match.
- Advanced operators cover arrays, dates, and SemVer precedence.
- All rule conditions must match.
- Percentage rollout is deterministic, including basis-point decimal percentages.
- JSON object and array values are evaluated through default, rule, and rollout behavior.
- Type mismatches return fallback.
Reference: ai/examples/good-sdk-client.md
Good SDK Client
Source: packages/sdk-js/src/index.ts (sha256: 4e37f26e5b5403cc60bce15951708fb83f8867da1618819afae4990295452f07)
Why this is canonical:
- Keeps config fetching and cache behavior inside the SDK client boundary.
- Uses ETag validators without reprocessing
304 Not Modified responses.
- Supports lazy loading by default while keeping manual, auto polling, and offline modes explicit.
- Preserves valid cache when refresh fails or remote config is invalid.
- Keeps localStorage persistent cache opt-in, scope-fingerprinted, and free of raw SDK keys or raw base URLs.
- Notifies subscribers only when a valid changed config replaces cache.
- Clears subscriptions when the client is closed.
- Delegates local evaluation to
@capture-flag/evaluator.
- Validates public config shape, including JSON object/array roots, while leaving fallback-safe targeting edge cases to the evaluator.
- Returns caller fallback instead of leaking SDK failures.
Canonical SDK client pattern from packages/sdk-js/src/index.ts.
export function createClient(options: CaptureFlagClientOptions): CaptureFlagClient {
const mode = options.mode ?? "lazy";
const storage = getStorage(options);
const cacheScope = createCacheScope(options.baseUrl, options.sdkKey);
let cacheEntry: CacheEntry | null = readStoredCache(
storage,
options.localStorageKey,
cacheScope,
);
const listeners = new Set<CaptureFlagConfigChangeListener>();
async function getConfig(): Promise<CaptureFlagConfig | null> {
if (mode === "offline" || mode === "manual") {
return cacheEntry?.config ?? null;
}
if (cacheEntry && (mode === "auto" || !isCacheExpired(cacheEntry, cacheTtlMs))) {
return cacheEntry.config;
}
await refreshConfig();
return cacheEntry?.config ?? null;
}
return {
async getValue(key, fallbackValue, context) {
try {
return evaluate({
config: await getConfig(),
context,
fallbackValue,
flagKey: key,
});
} catch {
return fallbackValue;
}
},
async refresh() {
await refreshConfig();
},
close() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
listeners.clear();
},
subscribe(listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
};
}
Refresh And ETag Pattern
async function fetchAndUpdateCache(): Promise<void> {
const response = await fetchConfig(options.baseUrl, options.sdkKey, cacheEntry?.etag ?? null);
if (response.status === 304) {
if (cacheEntry) {
writeCache({ ...cacheEntry, cachedAt: Date.now() });
}
return;
}
if (!response.ok) {
return;
}
const config = await response.json();
if (!isCaptureFlagConfig(config)) {
return;
}
replaceCache({
cachedAt: Date.now(),
config,
etag: response.headers.get("etag"),
});
}
The SDK sends If-None-Match only when a cached ETag exists, treats 304 as a freshness update, and validates config before replacing cache.
Subscription Pattern
function replaceCache(nextEntry: CacheEntry): void {
const previousEntry = cacheEntry;
writeCache(nextEntry);
if (!previousEntry || !cacheEntriesRepresentSameConfig(previousEntry, nextEntry)) {
notifyConfigChanged();
}
}
function notifyConfigChanged(): void {
for (const listener of Array.from(listeners)) {
try {
listener();
} catch {
}
}
}
Subscriptions are cache-change notifications only. They do not expose config internals, send evaluation context to the API, or notify for 304, failed refreshes, invalid configs, or equivalent config responses.
Persistent Cache Pattern
const storedValue: StoredCache = isStoredCache(parsedValue)
? parsedValue
: { entries: {}, schemaVersion: CACHE_SCHEMA_VERSION };
storedValue.entries[cacheScope] = entry;
storage.setItem(key, JSON.stringify(storedValue));
Persistent cache is opt-in through localStorageKey and stores a map of cache metadata plus config data keyed by scope fingerprint, not raw SDK keys or raw base URLs.
The SDK fetches config with the SDK key, keeps evaluation context local, preserves usable cache on refresh failures, and degrades to fallback when no safe config is available.