| name | add-core-module |
| description | Scaffold a new core module with api/impl split, optional test module, source files, and AGENTS.md. Use when adding shared infrastructure to the app. |
Add Core Module
Scaffold a core module with api/impl split for shared infrastructure.
Parameters:
name (lowercase kebab-case, e.g., feature-flag) — required
hasTest (boolean, default true) — whether to create a test/ submodule with fakes
plugin (mockdonalds.kmp.domain for modules needing Metro DI, mockdonalds.kmp.library for pure contracts) — default mockdonalds.kmp.domain
Reference Standards
- Architecture & module structure:
.agents/standards/architecture.md
- Naming conventions:
.agents/standards/naming-conventions.md
- DI patterns:
.agents/standards/dependency-injection.md
- CenterPost interactors:
.agents/standards/centerpost.md
- Convention plugins:
.agents/standards/convention-plugins.md
Reference Implementation
Use core/auth/ as the reference for api/impl split pattern, DI wiring, and AGENTS.md format.
Steps
1. Create Module Directories and Build Files
Create submodules under core/{name}/:
api/build.gradle.kts — public contract (interfaces, abstract interactors, types):
plugins {
id("mockdonalds.kmp.domain")
}
kotlin {
android {
namespace = "com.mockdonalds.app.core.{name}.api"
}
sourceSets {
commonMain.dependencies {
}
}
}
For pure contract modules with no DI needs, use mockdonalds.kmp.library instead.
impl/build.gradle.kts — concrete implementations:
plugins {
id("mockdonalds.kmp.domain")
}
kotlin {
android {
namespace = "com.mockdonalds.app.core.{name}.impl"
}
sourceSets {
commonMain.dependencies {
api(project(":core:{name}:api"))
}
}
}
test/build.gradle.kts (if hasTest is true) — fakes for consumer tests:
plugins {
id("mockdonalds.kmp.domain")
}
kotlin {
android {
namespace = "com.mockdonalds.app.core.{name}.test"
}
sourceSets {
commonMain.dependencies {
api(project(":core:{name}:api"))
api(project(":core:test-fixtures"))
}
}
}
2. Add Includes to settings.gradle.kts
Core modules are manually listed (not auto-discovered). Add to the "Core modules" section:
include(":core:{name}:api")
include(":core:{name}:impl")
include(":core:{name}:test")
3. Create Source Files
api/ — src/commonMain/kotlin/com/mockdonalds/app/core/{name}/
Place public interfaces, abstract interactors, and data types here. Package: com.mockdonalds.app.core.{name}.
- Interfaces define the contract — consumers depend only on these
- Abstract interactors extend
CenterPostInteractor or CenterPostSubjectInteractor
- Data classes use
@Serializable if they cross module boundaries
CenterPost Interactor Requirement
Core modules with api/impl expose CenterPost interactors for presenter consumption by default. Domain/data layers always inject the provider interface directly. One documented exception: core:remote-config uses Composable rememberFlag(flag) / rememberConfig(config) extensions instead of an interactor (see .agents/standards/centerpost.md for the rationale). New core modules follow the interactor rule unless the carve-out is re-justified and documented.
| Core Module Characteristic | Presenter Consumption | Domain/Data Consumption | Example |
|---|
| Produces observable state (Flow/StateFlow) | CenterPostSubjectInteractor | Provider interface | GetHomeContent (feature-level subject interactor) |
| Produces one-shot result or fire-and-forget | CenterPostInteractor | Provider interface | core:analytics: TrackAnalyticsEvent / AnalyticsDispatcher |
| Ambient config read (carve-out) | Composable extension on provider | Provider interface | core:remote-config: RemoteConfigProvider.rememberFlag(flag) / rememberConfig(config) over RemoteConfigProvider |
For fire-and-forget interactors, the inProgress loading state simply goes uncollected — it's opt-in with zero overhead. The value of wrapping even void operations in CenterPost: structured execution, error handling, timeout protection, dispatcher correctness.
impl/ — src/commonMain/kotlin/com/mockdonalds/app/core/{name}/impl/
Place concrete implementations here. Package: com.mockdonalds.app.core.{name}.impl.
Key rules:
test/ — src/commonMain/kotlin/com/mockdonalds/app/core/{name}/test/
Place fakes here (in commonMain, NOT commonTest). Package: com.mockdonalds.app.core.{name}.test.
- Fakes are
MutableStateFlow-backed with control methods (setXxx(), reset())
- Annotate with
@ContributesBinding(AppScope::class) for test graph auto-wiring
- Every public interface and abstract interactor in api needs a corresponding fake
- test modules depend on
api only — never on impl (Konsist-enforced via TestModuleDependencyTest). This keeps vendor SDKs and platform-specific code out of test graphs.
Parallel multibind contracts — if your impl module declares a multibind slot via a @ContributesTo(AppScope::class) interface with @Multibinds, declare the same slot a second time in test (different package, same scope, same signature). Test graphs that pull api + test (not impl) need to see the slot or Metro cannot resolve the Set<T>. Metro consolidates slots by type + scope, so both copies resolve to the same binding. Do this whenever a test module exists for a core that contributes multibind aggregators. See .agents/standards/dependency-injection.md → "Parallel multibind contracts".
4. Create Tests
Place unit tests in impl/src/commonTest/kotlin/com/mockdonalds/app/core/{name}/impl/.
- Use Kotest
BehaviorSpec with Given/When/Then structure
- Use inline anonymous-object fakes for internal interfaces
- Test all public behavior of each impl class
5. Create AGENTS.md
Create core/{name}/AGENTS.md following this template:
# core:{name}
## Purpose
One-line description of what this core module provides.
## Architecture
\```
core/{name}/api -> Public interfaces and types (feature-visible)
core/{name}/impl -> Concrete implementations (DI-only, never imported directly)
core/{name}/test -> Fakes for consumer tests
\```
## Public API
| Type | Module | Description |
|------|--------|-------------|
| `{Interface}` | api | Description |
| `{Interactor}` | api | Abstract CenterPost interactor for ... |
## Consumption Pattern
| Layer | What to inject | Why |
|-------|---------------|-----|
| Presenters | CenterPost interactor from api | Reactive, lifecycle-aware, structured execution |
| Domain/Data | Provider interface from api | Direct, synchronous access |
## Usage
**Presenters** use the CenterPost interactor:
\```kotlin
@CircuitInject(MyScreen::class, AppScope::class)
@Composable
fun MyPresenter(
myInteractor: {Interactor},
): MyUiState {
// Use interactor for reactive/one-shot access
}
\```
**Domain/data** inject the provider interface:
\```kotlin
@ContributesBinding(AppScope::class)
class MyRepositoryImpl(
private val myProvider: {Provider},
) : MyRepository {
// Use provider for direct access
}
\```
## Rules
- Core modules never import from features
- Features MUST depend on `core:{name}:api` only, never `core:{name}:impl`
- **Presenters** must use CenterPost interactors, never provider interfaces directly (Konsist-enforced)
- **Domain/data** must use provider interfaces, never CenterPost interactors (Konsist-enforced)
- `impl` is wired exclusively through Metro `@ContributesBinding` in `AppScope`
- Test code should use fakes from `core:{name}:test`
6. Wire into composeApp
Add the impl dependency to composeApp/build.gradle.kts in the core dependencies section:
implementation(project(":core:{name}:impl"))
This ensures the @ContributesBinding classes are on the classpath when ProdAppGraph is compiled. The api module is pulled transitively.
7. Verify Auto-Discovery
Confirm the modules are recognized:
./gradlew projects | grep {name}
Should show all submodules (api, impl, test if applicable).
Key Rules
- Package convention:
com.mockdonalds.app.core.{name} (api), .impl (impl), .test (test)
- No
@Inject on impl classes — @ContributesBinding handles it implicitly
- Fakes live in
test/src/commonMain/ — they are published dependencies, not test-only
- AGENTS.md is required (Konsist-enforced via
AgentDocumentationTest)
- Core modules NEVER import from feature modules
AppGraph in core:metro does NOT need updating — consumers get dependencies via constructor injection
Post-Change Verification — MANDATORY
Work is NEVER complete until verification passes. Run verify full since new modules touch build structure and require full validation.
The verify skill will run: lint, unit tests, architecture tests (Konsist + Harmonize), and builds for both platforms.
If ANY check fails, fix the issue and re-run. Do not declare the task complete until verification passes.