Release v1.0.0#220
Conversation
* Add develop branch to CI pipeline triggers Mirror main triggers onto develop for the new gitflow integration branch: ci, codeql, dependency-review build/scan on push/PR to develop, docs site is built on develop but published to Pages only from main or release tags, and publish.yml emits SNAPSHOTs from both branches while tagged release publication remains main-only. * Address review: skip CodeQL on workflow-only PRs, fix tag triggers CodeQL diff-range scanner fails with "no source code seen during build" when a PR touches no Java/Kotlin sources. Add paths-ignore for workflow, docs, and markdown files so docs/CI-config PRs no longer break the scan; weekly schedule still does a full scan. Move docs.yml tag patterns from pull_request to push — pull_request does not support tag filters, so tag-driven docs publication was unreachable despite the publish-docs if: clause checking for refs/tags/.
* Rename proguard task to generateFeaturedProguardRules Namespace the Gradle task with the `featured` prefix to avoid clashes with other plugins that register a similarly named task. The Kotlin class GenerateProguardRulesTask and the GENERATE_PROGUARD_TASK_NAME constant identifier stay the same — only the task name string and its references in tests and docs change. * Address review: changelog Unreleased entry and README scanAllLocalFlags Add `### Changed` entry under `## Unreleased` documenting the task rename with a one-line migration note. Append `scanAllLocalFlags` to the parenthesized task list in the Configuration Cache section of README so it matches the set of tasks actually registered by the plugin. Refs PR #190 review comments from qodo-code-review and copilot-pull-request-reviewer.
ConfigValues KDoc promises that observe wraps provider calls in try/catch
and routes errors through onProviderError, but the implementation collected
provider Flows raw, so a provider whose Flow throws downstream would crash
the host coroutine. Wrap local and remote Flows in .catch { onProviderError }
before merging to restore the documented contract. Add a regression test
asserting that a throwing local provider does not propagate and that the
error is reported.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Register enum converter for CheckoutVariant in sample The sample observes an enum-typed flag (CheckoutVariant) via DataStoreConfigValueProvider, but never registered a TypeConverter for the enum. DataStore preferences only support primitive keys, so observation crashed with IllegalArgumentException on launch. Register enumConverter<CheckoutVariant>() on the provider before building ConfigValues. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Register sample flags and fix status-bar overlap Register every flag in SampleFeatureFlags with FlagRegistry on Application start so the Debug UI renders the list instead of the empty state. Apply statusBarsPadding() to the home-screen top row so the "Debug flags" button is reachable under enableEdgeToEdge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The Gradle DSL accepts enum(...) flag declarations, but at runtime every local storage-backed provider needs an explicit registerConverter call with enumConverter<T>() to handle the enum — DataStore, JavaPreferences, and SharedPreferences all throw IllegalArgumentException otherwise, and NSUserDefaults has no converter API at all. Firebase handles enums automatically via reflection. This was not documented anywhere, so consumers hit a launch crash with no signal in the IDE. Extend the KDoc on FlagContainer.enum, enumConverter, and the plugin README to spell out the requirement and the iOS caveat. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- Replace 590-line README with a 75-line landing page that links to the Wiki and showcases a single end-to-end example
- Delete mkdocs.yml, docs/{guides,index,getting-started,ios-integration,known-limitations,changelog,api}, and the docs publish workflow — content moved to the Wiki at https://github.com/AndroidBroadcast/Featured/wiki
- Keep docs/cc-verification/ and docs/specs/ as internal audit/spec history
- Update Swift comments referencing docs/ios-integration.md to point at the new Wiki page
- Ignore secring.gpg, .build/, .claude runtime files in .gitignore
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Publish per-module Featured manifest as consumable artifact Adds a new consumable Gradle configuration `featuredManifest` (schema v1, Usage = "featured-manifest") that publishes a per-module `featured-manifest.json` description of the module's local and remote feature flags. Each `dev.androidbroadcast.featured` plugin application now registers a `generateFeaturedManifest` @CacheableTask that maps LocalFlagEntry rows from `flags.txt` into a self-contained FlagDescriptor list (key, propertyName, kind, valueType, defaultValue, enumTypeFqn) and serialises the result via kotlinx-serialization. The manifest is the producer side of the multi-module aggregation redesign: a follow-up PR introduces the aggregator that resolves all manifests through normal dependency resolution and generates the GeneratedFeaturedRegistry. The existing five flag-generation tasks are unchanged. A separate Maven-publish guard is intentionally omitted — custom consumable configurations are not auto-published by the Java / KMP / AGP software components, and a KMP smoke fixture gates the invariant that no `featured-manifest` variant appears in the published .module metadata. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review: normalize SDK path before writing local.properties cubic-dev-ai PR review (cf33824) flagged that FeaturedManifestIntegrationTest writes Android SDK path directly into local.properties via File.absolutePath, which on Windows yields raw backslashes — Java's .properties parser treats backslash as an escape character and would corrupt the path. Switch to File.invariantSeparatorsPath, which uniformly emits forward slashes. An identical pattern exists in the pre-existing FeaturedPluginIntegrationTest; that file is out of PR A scope and will be aligned in a separate cleanup PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Add aggregator plugin and GeneratedFeaturedRegistry codegen
Introduce dev.androidbroadcast.featured.application Gradle plugin that
aggregates featured-manifest.json artifacts from project dependencies
declared via featuredAggregation(project(...)) and generates a unified
object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> } in
build/generated/featured/commonMain/.
The aggregator reuses the FEATURED_MANIFEST_USAGE / schemaMajorAttr
contract published by dev.androidbroadcast.featured (PR A, schema v1)
through a resolvable featuredAggregationClasspath configuration.
generateFeaturedRegistry is @CacheableTask with stable (modulePath, key)
sort order; duplicate keys across the aggregated graph fail the build
naming both module paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Harden aggregator against malformed and hostile manifests
Apply finalize Round 1 findings across code-reviewer, /simplify, the
pr-review-toolkit trio, and the architecture/build/security experts:
- Escape newline / carriage-return / tab in escapeKotlinString so a
description or STRING default with literal whitespace cannot break
the generated source.
- Validate enumTypeFqn against Kotlin FQN grammar and ENUM defaultValue
against the Kotlin identifier grammar before codegen. A hostile
project dependency can no longer inject Kotlin source through the
featured-manifest.json payload that would execute on the next
compile in the consumer module.
- Sort manifests by modulePath inside validateUniqueKeys so the
duplicate-key error lists origins in a deterministic order regardless
of Gradle resolution order.
- Wrap manifest parse and read errors with the offending file path so
diagnostics name which artifact failed.
- Thread the offending key + module path into the requireNotNull error
raised when an ENUM descriptor is missing its enumTypeFqn.
- Validate outputPackage against a Kotlin package-name regex at task
execution time instead of waiting for the consumer compile to fail.
- Narrow FeaturedApplicationPlugin to internal — Gradle still resolves
the descriptor by FQN via reflection, and the consumer-facing surface
is the plugin id alone.
- Remove the redundant pre-sort and dead Origin class in the task; the
generator does its own (modulePath, key) sort, and the validator now
owns ordering for its own error path.
- Drop a stale inline comment that paraphrased the generator KDoc.
Tests:
- Newline, carriage return, and tab escape tests for STRING defaults
and descriptions.
- Parse-error test asserting the wrapper carries the file path.
- Descriptor-integrity tests covering enumTypeFqn / defaultValue
injection attempts (semicolon, angle bracket, parenthesis, brace,
whitespace, Unicode line separator, missing FQN).
- Lazy-realization test mirroring the producer-side pattern.
:featured-gradle-plugin:check — BUILD SUCCESSFUL, 266 tests, 0 failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Address review: primitive default injection + enum classpath docs
cubic #1 and Copilot #3: extend validateFlagDescriptorIntegrity from
ENUM-only to an exhaustive when over all seven ValueTypes. Boolean,
Int, Long, Float, and Double defaultValue strings are now validated
against their respective Kotlin literal grammars before reaching the
codegen. A malicious project dependency can no longer smuggle Kotlin
through a primitive default like "0.also { ... }" for an Int flag or
"false; init { ... }; private val x = true" for a Boolean. STRING is
the only remaining unchecked branch — already neutralised by
escapeKotlinString.
Copilot #4: document that featuredAggregation(project(...)) resolves
only the featured-manifest variant, not the producer's main runtime.
Modules with enum flags additionally require implementation(project)
in the consumer for the enum class to be on the compile classpath.
Plugin KDoc and the CHANGELOG entry both call this out.
Copilot #5: fix GeneratedFeaturedRegistryGenerator KDoc that claimed
to import enum FQNs. The generator imports only ConfigParam and
references enum types inline by FQN; KDoc now matches.
Tests: 10 new primitive-literal cases — true/false pass, injection
shapes throw, negative ints pass, Long max pass, scientific-notation
double pass, brace/method-call shapes throw.
:featured-gradle-plugin:check — BUILD SUCCESSFUL, 0 failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
PR C of 4-PR redesign of Featured flag registry. - FeatureFlagsDebugScreen now takes explicit registry: List<ConfigParam<*>> parameter; pass GeneratedFeaturedRegistry.all (from dev.androidbroadcast.featured.application plugin) or build inline. Each ConfigParam.key must be unique; LaunchedEffect keys via equals(). - Removed featured-registry module (FlagRegistry singleton + expect/actual FlagRegistryDelegate). - Removed generateFlagRegistrar task, FlagRegistrarGenerator, GenerateFlagRegistrarTask, dead LocalFlagEntry.kotlinReference. - Sample now exposes SampleFeatureFlags.all; registerSampleFlags() removed. - Docs synced (CHANGELOG, README, wiki, CLAUDE.md). CI cleanup: dropped stale :featured-registry:koverVerify from ci.yml. Breaking change; Featured does not keep API compatibility.
* Extract featured-gradle-plugin to build-logic included build
Moves featured-gradle-plugin/ to build-logic/featured-gradle-plugin/ and
wires it via pluginManagement.includeBuild("build-logic") so subprojects
in the main build can apply id("dev.androidbroadcast.featured") without
a version coordinate. Unblocks the multi-module sample restructure that
needs the plugin available to feature modules in the same build.
build-logic has its own settings file with the version catalog reused
from the parent (from(files("../gradle/libs.versions.toml"))) and a
gradle.beforeProject hook that propagates parent gradle.properties
(notably VERSION_NAME) so the plugin still publishes with the project
version instead of "unspecified".
Root publishToMavenCentral / publishToMavenLocal proxy tasks delegate
to the included build so the publish workflow keeps a single entrypoint.
The plugin constraint is dropped from featured-bom: a java-platform BOM
only constrains dependency resolution and is irrelevant to plugins
applied through pluginManagement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Split sample flags into three KMP feature modules
Introduces :sample:feature-checkout, :sample:feature-promotions, and
:sample:feature-ui as canonical demo of the Featured aggregator workflow:
each module applies the plugin, declares its own flags via the
featured { localFlags / remoteFlags } DSL, and exposes a public
observe-bridge file (Flow<T>-shaped) on ConfigValues. The bridges are
the pedagogical surface — GeneratedLocalFlags/GeneratedRemoteFlags
contain ConfigParam<T> instances that consumers compose into Flow via
observe(...).map { it.value }.
Fixes a latent codegen bug surfaced by the first cross-platform use of
GeneratedFlagExtensions: extensions were emitted non-suspend but
ConfigValues.getValue is suspend, so any Kotlin/Native or AndroidMain
compile failed. ExtensionFunctionGenerator now emits suspend extensions,
ConfigParamGenerator widens GeneratedLocalFlags / GeneratedRemoteFlags
to public so they can be referenced from observer bridges in other
modules, and the generated extensions file name now includes a module-
derived suffix (e.g. GeneratedFlagExtensionsSampleFeatureCheckout.kt) so
each module's JVM class name is unique without relying on @file:JvmName
(which doesn't resolve in commonMain on KMP iOS targets).
GenerateConfigParamTask now wipes its output directory on each run so
file-name changes don't leave stale generated sources behind.
Out of scope (tracked as follow-up): ProguardRulesGenerator and
featured-shrinker-tests still target the legacy non-suspend extension
signature; R8 per-function DCE via -assumevalues silently stops
matching the new suspend JVM signature until that subsystem is reworked.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Convert :sample:shared to Featured aggregator
Applies dev.androidbroadcast.featured.application to :sample:shared
and replaces the hand-written SampleFeatureFlags with the plugin-
generated GeneratedFeaturedRegistry.all sourced via featuredAggregation
from :sample:feature-checkout, :sample:feature-promotions, and
:sample:feature-ui.
The three feature modules are also added as api dependencies because
CheckoutVariant and the observe-bridge extensions defined in them
appear in the public surface that downstream sample app modules
(:sample:android-app, :sample:desktop) consume directly. The iOS
framework blocks export the three modules so Swift sees CheckoutVariant
without needing a separate Pod.
generateFeaturedRegistry produces an object with five ConfigParam
entries (sorted by modulePath then key) which the debug screen and
ViewModel now consume in Phase 5.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Migrate sample consumers to observe-bridges; module-suffix flag objects
Phase 5 of PR D rewires the sample ViewModel and screens to consume
flags through the new observe-bridge pattern instead of the deleted
hand-written SampleFeatureFlags. SampleViewModel now exposes five
StateFlow<T> built from the per-module Flow bridges with stateIn
defaults that mirror each feature module's DSL default, plus four
suspend-launching setters routed through ConfigValues.override.
FeaturedSample picks up CheckoutVariant from its new home in
:sample:feature-checkout; MainActivity points FeatureFlagsDebugScreen
at GeneratedFeaturedRegistry.all.
A second collision surfaced during the first Android assembleDebug:
ConfigParamGenerator emitted a fixed-name GeneratedLocalFlags /
GeneratedRemoteFlags object in the same package across every module,
so feature-checkout and feature-ui produced duplicate dex classes
under the same FQN. ConfigParamGenerator now names both the file and
the public object with a module-derived suffix (e.g.
GeneratedLocalFlagsSampleFeatureCheckout), matching the
GeneratedFlagExtensions fix from Phase 3. The three observe-bridge
files reference the suffixed objects accordingly.
The matching iOS link crash in ObjCExportCodeGenerator was the
side-effect of the previous implementation-vs-api wiring: K/N could
not resolve the concrete type adapter for ConfigParam<CheckoutVariant>
because the feature klibs weren't on the link path. Phase 4's
api(project(":sample:feature-*")) switch and Phase 3b's matching
visibility/api widening jointly fix it — a clean
linkDebugFrameworkIosSimulatorArm64 now succeeds without changes here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Add enum override input to FeatureFlagsDebugScreen
ConfigParam<E> now carries enumConstants: List<E>? so the debug UI can
discover the legal values without runtime reflection. Plugin codegen
fills the field for both per-module objects and the aggregated
GeneratedFeaturedRegistry by emitting kotlin.enumValues<EnumFqn>()
.toList() for any flag with enumTypeFqn in the manifest.
FeatureFlagsDebugScreen dispatches to a new EnumDropdown
(ExposedDropdownMenuBox + DropdownMenuItem) whenever
param.enumConstants != null, mirroring the boolean/string/int input
treatment: current value visible in the text field, options in the
dropdown, source badge updates to LOCAL on selection. Boolean and
scalar input paths are unchanged.
Verified on Pixel 10 emulator: the sample's checkout_variant enum can
be flipped to NEW_SINGLE_PAGE / NEW_MULTI_STEP via the debug screen,
the main screen reacts immediately, and DataStore persistence survives
both rotation and cold restart.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Document multi-module sample structure
Updates the [Unreleased] CHANGELOG with the PR D shape: three sample
feature modules, build-logic-extracted plugin, enum override input in
the debug screen, and the module-suffixed generator filenames. Adds a
sample/CLAUDE.md as the short navigational note for future sessions
working on the sample (module map, observe-bridge convention, how to
add a flag). The GitHub wiki pages Sample-App.md and Multi-Module-
Setup.md are updated in the separate .wiki repo alongside this PR.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Drop dead listOf wrapper around iOS target declarations
The three sample feature modules wrapped iosX64() / iosArm64() /
iosSimulatorArm64() in a listOf(...) whose result was discarded.
The constructors register the targets via side-effect; the wrapper
was leftover from copying :sample:shared (which uses .forEach to
configure framework binaries). In the leaf modules the wrapper
reads as if a .forEach { ... } was deleted. Replace with bare
statements to match the idiom used elsewhere.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Track parent gradle.properties via providers.fileContents
build-logic/settings.gradle.kts read the root gradle.properties via
raw java.util.Properties().load(FileInputStream), which the
configuration cache does not fingerprint. After a VERSION_NAME bump
the included build could publish the cached previous version.
Switch to providers.fileContents(...).asText so Gradle invalidates
the cache when the parent file changes. pluginManagement {} also
moves to the first block as the docs require.
Cover the new ConfigParam.enumConstants field in equals, hashCode,
and toString with focused unit tests so the :core ≥90% line-coverage
gate stays green and the equality semantics — registry dedup depends
on them — are pinned.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Address review: switch generators to kotlin.enums.enumEntries
cubic flagged that the codegen emits the deprecated kotlin.enumValues<T>()
helper. Replace with kotlin.enums.enumEntries<T>() (stable since Kotlin
1.9): it returns EnumEntries<T> — a lazy, cached List<T> — so the
trailing .toList() is no longer needed. Generated ConfigParam<E> objects
in per-module GeneratedLocalFlags* and in the aggregated
GeneratedFeaturedRegistry both benefit; ConfigParam.enumConstants still
types as List<T>? unchanged. Tests pin the new emit string.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…201) * Add sync getValueCached read path on ConfigValues ConfigValues now owns an in-memory snapshot (Map<String, ConfigValue<*>> held in kotlin.concurrent.AtomicReference, copy-on-write) and exposes getValueCached(...) — a non-suspend reader that returns the cached ConfigValue or a DEFAULT-sourced fallback before any warm-up write. Reads are safe on any thread including the Android main thread. override(), fetch(), and suspend getValue() all write-through into the snapshot so subsequent sync reads reflect the latest value with the Source the provider reported. resetOverride() clears the local override and dispatches a background coroutine (Dispatchers.Default) that re- resolves the param through the provider priority chain, refreshing the snapshot to whatever remote / default value is current. ConfigValues now owns a SupervisorJob-backed CoroutineScope and implements AutoCloseable so the scope can be torn down deterministically. isEnabled extension is no longer suspend — it delegates to getValueCached so generated is*Enabled() codegen can return to a non-suspend shape (preparing the path for restoring R8 per-function DCE in later phases). Callers that previously awaited isEnabled lose the suspend boundary; Featured does not keep API compatibility. initialize() snapshot warm-up still depends on providers implementing the upcoming SnapshotConfigValueProvider — Phase 2 wires that. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Generate non-suspend is*Enabled extensions calling getValueCached ExtensionFunctionGenerator stops emitting suspend on the per-flag extensions and switches the underlying call from suspend getValue to the new non-suspend getValueCached (added in the previous commit). The generated is<Name>Enabled() / get<Name>() helpers can now be called from any context — including @composable bodies — without dragging a coroutine scope along. The JVM method signature returns to plain `boolean is<Name>Enabled(ConfigValues)` (no Continuation parameter), so the ProGuard -assumevalues rule shape that ProguardRulesGenerator already emits matches the real method again and R8 can resume per-function DCE on dead-flag branches. The shrinker-tests subsystem and the ProGuard rule format itself need no changes — Phase 4 just removes the "no-op" caveats from the relevant KDoc. Generator unit tests pin the new non-suspend / getValueCached shape; end-to-end sample assemble (Android / Desktop / iOS sim) stays green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Point ProGuard assumevalues rules at the real generated class name ProguardRulesGenerator derived the target Kotlin file class name via ExtensionFunctionGenerator.jvmFileName, which reproduced the old @file:JvmName pattern that PR D removed. The real compiled class is named after the source file (e.g. GeneratedFlagExtensionsSampleFeatureUiKt), so R8 silently no-op'd the -assumevalues directive — per-function dead- code elimination did not actually happen in consumer builds. Module paths with hyphens additionally produced JVM-illegal identifiers in the emitted rules, which R8 also ignored without warning. Switch the rule target to the file-name-derived class (drop jvmFileName, reuse ExtensionFunctionGenerator.fileName so the sanitization stays in one place) and update SyntheticBytecodeFactory + the R8 elimination tests to match. The shrinker-tests suite now exercises R8 against bytecode whose JVM class name matches what the rules target — the pipeline is consistent again and the existing R8 DCE assertions are finally exercising the production shape. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Document sync ConfigValues read path Records the [Unreleased] CHANGELOG entries for the sync ConfigValues work: new getValueCached reader, non-suspend isEnabled extension, generated codegen losing suspend, AutoCloseable on ConfigValues, and the R8 DCE ProGuard rule fix. The sample CLAUDE.md gains a one- paragraph pointer noting that non-reactive call sites should use getValueCached or the codegen extensions, while reactive UI keeps the observe-bridge pattern that already exists in the multi-module sample. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Re-resolve in resetOverride synchronously; drop AutoCloseable Phase A code review flagged that the backgroundScope.launch in resetOverride could clobber a concurrent override(p, X) write: the background re-resolve writes the just-resolved REMOTE value after the caller's LOCAL=X write lands, and snapshot/provider diverge until the next getValue. resetOverride is already suspend, so do the re-resolve inline — the caller pays the same cost and the ordering becomes deterministic. The explicit writeSnapshot call stays because getValue intentionally does not write DEFAULT into the snapshot; without the explicit write a previously overridden slot would stay stale when both providers return null. The synchronous re-resolve makes the internal CoroutineScope unused, so ConfigValues no longer implements AutoCloseable. The CHANGELOG entry is swapped accordingly. observe()'s KDoc gets a more accurate description of write-through behaviour, and the concurrent-write test is renamed to reflect what runTest actually exercises (single-thread coroutine interleaving, not OS-level parallelism). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Generate flag objects as internal Flip the visibility of generated GeneratedLocalFlags* and GeneratedRemoteFlags* objects from public to internal. A module's flag declarations are an implementation detail; cross-module introspection flows exclusively through GeneratedFeaturedRegistry.all. Update ConfigParamGenerator KDoc, ExtensionFunctionGenerator KDoc, and ConfigParamGeneratorTest to match the new contract. * Split SampleViewModel into per-feature VMs and wire per-module ConfigValues Each :sample:feature-* module now owns its ViewModel and ConfigValues: - CheckoutFlagsViewModel in :sample:feature-checkout - PromotionsFlagsViewModel in :sample:feature-promotions - UiFlagsViewModel + MainButtonColor in :sample:feature-ui SampleApp/FeaturedSample accept three per-feature VMs as parameters instead of a single shared ConfigValues. Platform shells (Activity, desktop main, iOS UIViewController) construct four ConfigValues from one shared local provider and pass them to the appropriate VMs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Document multi-module ConfigValues wiring pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix stale SampleViewModel references in sample/CLAUDE.md * Address finalize Round 1 Phase A findings - UiFlagsViewModel.mainButtonColor: fix initial value Blue → Red to match flagActive's true default (no cold-start flicker) - Desktop and iOS shells: add debugConfigValues for pattern consistency per plan §10.3/§10.4 * Address finalize Round 1 Phase B findings - Revert Phase A finding 3: dead Desktop/iOS debugConfigValues vars; doc-only note that those shells omit the debug aggregator since they have no debug-UI entry - FeaturedSample: internal (only used by SampleApp wrapper in same module) - MainButton: private (only used in same file) - MainViewController.kt: drop RedundantVisibilityModifier suppression (public required by explicit-API mode) - sample/CLAUDE.md: clarify debug aggregator is Android-only * Address finalize Round 1 Phase C findings - UiFlagsViewModel: remove flagActive public API (dual-exposure of Boolean + MainButtonColor); make setter accept MainButtonColor instead of Boolean for symmetric vocabulary - MainButtonColor: drop unused companion object Default (contradicted ViewModel's initialValue) - FeaturedSample: derive activate locally from buttonColor, call setMainButtonColor with domain type - ConfigParamGeneratorTest: add symmetric remote-public-val assertion (parity with local) * Address review: comment + README accuracy fixes - README: snippet uses defaultLocalProvider(applicationContext) instead of incorrect DataStoreConfigValueProvider(context, ...) (real signature takes DataStore<Preferences>) - sample/shared/build.gradle.kts: rewrite stale api(:core) rationale — :core is referenced from iosMain (MainViewController) and via per-feature VM constructors, not SampleApp's signature - MainViewController.kt: drop misleading "iOS equivalent of Application scope" wording; UIViewController lifetime is screen/session, not app-wide --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
build-logic/ is the conventional location for internal convention plugins, not a published Maven Central artifact. Moves the plugin to its own included build at featured-gradle-plugin/ (the previously empty directory), matching the standard OSS Gradle plugin layout. Dogfooding on :sample:feature-* remains via includeBuild(...) — no functional change. Removes the empty build-logic/ wrapper. Updates root settings.gradle.kts, build.gradle.kts and CLAUDE.md to reflect the new structure.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
5 issues found across 144 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="CONTRIBUTING.md">
<violation number="1" location="CONTRIBUTING.md:73">
P2: The Versioning Policy table states all breaking API changes require a MAJOR bump, but the new API Stability section immediately contradicts this by saying iOS/JVM Preview breaking changes can land in MINOR releases. Readers will be confused about which rule to follow — the table has no 'Preview' carve-out.</violation>
</file>
<file name="featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt">
<violation number="1" location="featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt:56">
P1: Deleting the entire shared generated directory removes outputs from other tasks (e.g. `FeatureFlagExpect.kt`), making builds order-dependent and potentially failing compilation.</violation>
</file>
<file name="core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValuesExtensions.kt">
<violation number="1" location="core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValuesExtensions.kt:62">
P3: KDoc incorrectly claims `fetch()` warms the snapshot; this can mislead users about when `isEnabled()` reflects fetched values.</violation>
</file>
<file name="core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt">
<violation number="1" location="core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt:141">
P2: `enumConstants` is publicly writable for any `ConfigParam<T>`, which allows invalid non-enum metadata and can crash debug UI enum rendering via unchecked `Enum` casts.</violation>
</file>
<file name="core/src/commonTest/kotlin/dev/androidbroadcast/featured/ProviderErrorObserveTest.kt">
<violation number="1" location="core/src/commonTest/kotlin/dev/androidbroadcast/featured/ProviderErrorObserveTest.kt:75">
P2: The test cancels collection in a way that can hide a propagated terminal error, so it may pass even if `observe()` still throws after the second emission.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Re-trigger cubic
| // Clean before writing — the extension file name changed from the fixed | ||
| // "GeneratedFlagExtensions.kt" to a module-specific name, so stale files | ||
| // from previous runs must be removed to avoid duplicate-class compile errors. | ||
| dir.deleteRecursively() |
There was a problem hiding this comment.
P1: Deleting the entire shared generated directory removes outputs from other tasks (e.g. FeatureFlagExpect.kt), making builds order-dependent and potentially failing compilation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt, line 56:
<comment>Deleting the entire shared generated directory removes outputs from other tasks (e.g. `FeatureFlagExpect.kt`), making builds order-dependent and potentially failing compilation.</comment>
<file context>
@@ -48,19 +50,24 @@ public abstract class GenerateConfigParamTask : DefaultTask() {
+ // Clean before writing — the extension file name changed from the fixed
+ // "GeneratedFlagExtensions.kt" to a module-specific name, so stale files
+ // from previous runs must be removed to avoid duplicate-class compile errors.
+ dir.deleteRecursively()
dir.mkdirs()
</file context>
| dir.deleteRecursively() | |
| dir.listFiles()?.forEach { file -> | |
| val name = file.name | |
| if ( | |
| name == "GeneratedFlagExtensions.kt" || | |
| name.startsWith("GeneratedLocalFlags") || | |
| name.startsWith("GeneratedRemoteFlags") || | |
| name.startsWith("GeneratedFlagExtensions") | |
| ) { | |
| file.delete() | |
| } | |
| } |
|
|
||
| Example timeline: deprecated in `1.2.0` → error in `1.3.0` → removed in `2.0.0`. | ||
| - **Android (Stable):** a breaking public-API change (removed/renamed symbol, changed signature) requires a `MAJOR` version bump. | ||
| - **iOS (Preview) and JVM (Preview):** public API may change in `MINOR` releases without a major bump; no migration window is provided. |
There was a problem hiding this comment.
P2: The Versioning Policy table states all breaking API changes require a MAJOR bump, but the new API Stability section immediately contradicts this by saying iOS/JVM Preview breaking changes can land in MINOR releases. Readers will be confused about which rule to follow — the table has no 'Preview' carve-out.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CONTRIBUTING.md, line 73:
<comment>The Versioning Policy table states all breaking API changes require a MAJOR bump, but the new API Stability section immediately contradicts this by saying iOS/JVM Preview breaking changes can land in MINOR releases. Readers will be confused about which rule to follow — the table has no 'Preview' carve-out.</comment>
<file context>
@@ -63,16 +63,14 @@ Featured follows [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`
-Example timeline: deprecated in `1.2.0` → error in `1.3.0` → removed in `2.0.0`.
+- **Android (Stable):** a breaking public-API change (removed/renamed symbol, changed signature) requires a `MAJOR` version bump.
+- **iOS (Preview) and JVM (Preview):** public API may change in `MINOR` releases without a major bump; no migration window is provided.
## Releasing a New Version
</file context>
| description: String? = null, | ||
| category: String? = null, | ||
| since: String? = null, | ||
| enumConstants: List<T>? = null, |
There was a problem hiding this comment.
P2: enumConstants is publicly writable for any ConfigParam<T>, which allows invalid non-enum metadata and can crash debug UI enum rendering via unchecked Enum casts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigParam.kt, line 141:
<comment>`enumConstants` is publicly writable for any `ConfigParam<T>`, which allows invalid non-enum metadata and can crash debug UI enum rendering via unchecked `Enum` casts.</comment>
<file context>
@@ -131,6 +138,7 @@ public inline fun <reified T : Any> ConfigParam(
description: String? = null,
category: String? = null,
since: String? = null,
+ enumConstants: List<T>? = null,
): ConfigParam<T> =
ConfigParam(
</file context>
| assertEquals(ConfigValue.Source.LOCAL, fromLocal.source) | ||
| collected.add(fromLocal.value) | ||
|
|
||
| cancelAndIgnoreRemainingEvents() |
There was a problem hiding this comment.
P2: The test cancels collection in a way that can hide a propagated terminal error, so it may pass even if observe() still throws after the second emission.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At core/src/commonTest/kotlin/dev/androidbroadcast/featured/ProviderErrorObserveTest.kt, line 75:
<comment>The test cancels collection in a way that can hide a propagated terminal error, so it may pass even if `observe()` still throws after the second emission.</comment>
<file context>
@@ -0,0 +1,90 @@
+ assertEquals(ConfigValue.Source.LOCAL, fromLocal.source)
+ collected.add(fromLocal.value)
+
+ cancelAndIgnoreRemainingEvents()
+ }
+
</file context>
| * [ConfigParam.defaultValue] before the snapshot is warmed by [ConfigValues.getValue], | ||
| * [ConfigValues.fetch], or [ConfigValues.override] — matching Firebase Remote Config's |
There was a problem hiding this comment.
P3: KDoc incorrectly claims fetch() warms the snapshot; this can mislead users about when isEnabled() reflects fetched values.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At core/src/commonMain/kotlin/dev/androidbroadcast/featured/ConfigValuesExtensions.kt, line 62:
<comment>KDoc incorrectly claims `fetch()` warms the snapshot; this can mislead users about when `isEnabled()` reflects fetched values.</comment>
<file context>
@@ -58,8 +58,14 @@ public fun <T : Any> ConfigValues.asStateFlow(
- * This is a convenience wrapper around [ConfigValues.getValue] for [Boolean] parameters,
- * eliminating the need to unwrap the [ConfigValue] envelope at every call site.
+ * This is a **synchronous**, non-suspend read from the in-memory snapshot. It returns
+ * [ConfigParam.defaultValue] before the snapshot is warmed by [ConfigValues.getValue],
+ * [ConfigValues.fetch], or [ConfigValues.override] — matching Firebase Remote Config's
+ * "activate-then-read" contract.
</file context>
| * [ConfigParam.defaultValue] before the snapshot is warmed by [ConfigValues.getValue], | |
| * [ConfigValues.fetch], or [ConfigValues.override] — matching Firebase Remote Config's | |
| * [ConfigParam.defaultValue] before the snapshot is warmed by [ConfigValues.getValue] | |
| * or [ConfigValues.override]. A plain [ConfigValues.fetch] call does not populate the | |
| * snapshot until a subsequent [ConfigValues.getValue] / [ConfigValues.observe] read occurs. |
Release v1.0.0 — develop → main
First stable release of Featured (KMP feature-flag / configuration library). Brings
mainup from1.0.0-Beta1to the full current architecture (registry redesign #197–#202, plugin moved to repo root #215) plus release prep (#219).Platform positioning
What's in 1.0.0 (since Beta1)
featured-manifest.jsonproducer +dev.androidbroadcast.featured.applicationaggregator →GeneratedFeaturedRegistry.all(Publish per-module Featured manifest (producer side) #197, Add aggregator plugin and GeneratedFeaturedRegistry codegen #198).FlagRegistrysingleton;FeatureFlagsDebugScreenis now UI-agnostic, takes an explicit registry list (Remove FlagRegistry; make FeatureFlagsDebugScreen UI-agnostic #199).ConfigValuespattern (Multi-module sample showcasing Featured aggregator plugin #200).getValueCached/ non-suspendis*Enabled()(Restore R8 per-function DCE: sync getValueCached + ProGuard rule fix #201).ConfigValues; generated flag objects are nowinternal(Per-module ConfigValues + internal generated objects #202).build-logic/to a repo-root included build (Move Gradle plugin from build-logic/ to repo root #215).ConfigValues.observe()catches provider exceptions (Fix ConfigValues.observe to catch provider exceptions #196); user docs migrated to the Wiki (Migrate user documentation to GitHub Wiki #193); ProGuard task renamed togenerateFeaturedProguardRules(Rename proguard task to generateFeaturedProguardRules #190).VERSION_NAME=1.0.0, CHANGELOG[1.0.0], README Platform stability table, corrected false BCV-enforcement claim in CLAUDE.md/CONTRIBUTING.md (Prepare v1.0.0 release #219).Verification (all green)
assemble,test,spotlessCheck,:core:koverVerify(≥90%), plugin tests,publishToMavenLocal,:core:zipXCFramework— all green.FeaturedSampleApp.frameworkbuild.Release mechanics (after merge)
Maven version is derived from the
v1.0.0tag (notgradle.properties). Tagging triggerspublish.yml: Central staging (manual promotion), XCFramework GitHub Release, and a bot commit updatingPackage.swiftonmain.Note (known, accepted for this preview iOS release): SPM consumers of
v1.0.0tag will resolve the Beta1 XCFramework (Package.swift is updated by a child commit after the tag). iOS Wiki will document consuming via the release asset / commit pin. SemVer/Android/Maven are unaffected.Recommended merge method: merge commit (preserve develop history on main, tag the merge commit).