Skip to content

Check a remote config wires up every flag, without running the app - #36

Merged
andyyhope merged 3 commits into
mainfrom
feature/mapping-audit
Aug 25, 2026
Merged

Check a remote config wires up every flag, without running the app#36
andyyhope merged 3 commits into
mainfrom
feature/mapping-audit

Conversation

@andyyhope

Copy link
Copy Markdown
Owner

A large config file has too many values to trace by hand, and two ways for one to
be wrong that no single assertion catches. A flag whose remoteKey matches nothing
is not an error — it falls through to its default, which looks exactly like a
backend that sent nothing. And a value in the file that no flag reads is invisible
until someone notices the feature never turned on.

let audit = try FlagMappingAudit(AppFlags.self, applying: json)
print(audit)
// Flag mapping audit — incomplete.
//   absent (1) — a flag declares this path, the payload has nothing there:
//     • page-size
//   mismatched (1):
//     • 'config.tier' → checkout-tier: "gold" is not one of free, pro
//   unconsumed (2) — usually fine, often backend metadata — no flag reads these:
//     • config.page_size
//     • meta.version

Both directions

  • absent (forward): a flag declares a remoteKey the payload does not supply.
    Almost always a path typo, otherwise indistinguishable from a value the backend
    chose not to send.
  • unconsumed (reverse): a value in the file that no flag reads. The same typo
    usually shows here too, which is what makes it findable.

The audit never checks types itself — it reuses the validation RemoteOverrideSource
applies, extracted into FlagSchema.mapRemote so the two cannot drift. A value it
calls applied is one a real apply would accept; any type or enum-case problem
arrives as a RemoteOverrideProblem in mismatched, the whole list at once.

Lenient by default

A real config carries metadata no flag reads, so unconsumed values are reported but
do not fail isComplete. strict: true requires every value to be read;
ignoring: ["meta"] excuses the prefixes a file is expected to carry.

A record flag's remoteKey names a subtree, so every leaf beneath it counts as read
— a keyed list of a hundred endpoints is not a hundred unconsumed values.

Packaging

FlagMappingAudit lives in FeatureFlag, so any test target already reaches it, and
requireComplete() suits a debug-build check at launch. FeatureFlagTestSupport adds
XCTAssertFlagsFullyMapped, which reports the whole audit at the call site; it links
XCTest behind canImport, builds for every platform, and belongs only in a test
target.

Adversarial review

Two defects in the leaf-path walker, both at degenerate roots. An empty payload {}
reported one unconsumed value that was the empty string — a spurious strict-mode
failure and a blank bullet in the report. A root-level array produced paths with a
leading dot. Both fixed; a nested empty container still has a real path and stays
reported.

Verified

743 tests on a clean build. DocC clean with --warnings-as-errors, both example apps
build, and the iOS package scheme builds with the new XCTest-linking product. The
demo's own three bundled configs audit clean end to end, with a test proving a
misspelled path is caught from both sides.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD

andyyhope and others added 3 commits August 25, 2026 13:39
A large config file has too many values to trace by hand, and two ways for one
to be wrong that no single assertion catches. A flag whose remoteKey matches
nothing is not an error — it falls through to its default, which looks exactly
like a backend that sent nothing. And a value in the file that no flag reads is
invisible until someone notices the feature never turned on.

FlagMappingAudit reports both directions. It reuses the validation
RemoteOverrideSource applies — extracted into FlagSchema.mapRemote so the two
cannot drift — so a value it calls mapped is one a real apply would accept, and
types are never the audit's concern. What it adds is coverage: absent flags
(forward), and unconsumed values (reverse, from the declared remoteKey paths).

Unconsumed values are reported but do not fail by default, because a real config
carries metadata no flag reads. `strict` requires every value to be read;
`ignoring` excuses the prefixes a file is expected to carry. The same typo often
shows from both sides — an absent flag and an unconsumed value — which is what
makes it findable.

FeatureFlagTestSupport adds XCTAssertFlagsFullyMapped, which reports the whole
audit at the call site. It links XCTest behind canImport, so it builds for every
platform and belongs only in a test target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD
A DocC article on validating a remote config — both coverage directions, why an
unconsumed value usually is not a fault, and how to run it from a test or a debug
build. Linked from Remote overrides and the topics, and the README lists it.

The demo's bundled payloads are now audited end to end: every one wires up every
flag it provides, passes strict with nothing unconsumed, and a deliberately
misspelled path is caught as both an absent flag and an unconsumed value. This is
the pattern to copy for a real config, run against the real demo config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD
The leaf walker treated an empty root object as a leaf with an empty path, so an
empty payload reported one unconsumed value that was the empty string — a spurious
strict-mode failure, and a blank bullet in the report. A root-level array produced
paths with a leading dot, since the array branch did not guard the empty prefix the
way the object branch did.

Both fixed. A nested empty container still has a real path and stays reported,
since a flag may have expected something beneath it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD
@andyyhope
andyyhope merged commit 528b58b into main Aug 25, 2026
6 checks passed
@andyyhope
andyyhope deleted the feature/mapping-audit branch August 25, 2026 03:55
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.

1 participant