Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c71ab9b
feat: Implement ScopeAggregate refactoring with event sourcing
kamiazya Sep 22, 2025
755659c
feat: Implement ES decision + RDB projection pattern with EventProjector
kamiazya Sep 23, 2025
341c17c
Merge main branch: contracts serialization fix and CI updates
kamiazya Sep 23, 2025
ff8d97b
refactor: Implement EventProjector → EventPublisher pattern and fix K…
kamiazya Sep 23, 2025
001265b
fix: Replace null assertions with safe error handling
kamiazya Sep 23, 2025
fe77a8a
refactor: extract duplicate string literals as constants
kamiazya Sep 23, 2025
244b824
feat: Add critical validation based on AI review feedback
kamiazya Sep 23, 2025
52e6627
fix: Resolve test failures by simplifying CreateContextViewUseCaseTest
kamiazya Sep 23, 2025
92785f8
test: make empty description validation assertion concrete
kamiazya Sep 23, 2025
09d0490
feat: Implement four architectural improvements for robustness and in…
kamiazya Sep 23, 2025
33f1ee1
fix: Replace throw statement with error() function in UpdateScopeHandler
kamiazya Sep 23, 2025
009c226
refactor: reduce CLI ErrorMessageMapper cognitive complexity from 29 …
kamiazya Sep 23, 2025
5e0bc6f
feat: Implement comprehensive ES+RDB architectural improvements
kamiazya Sep 24, 2025
7005aaf
refactor: reduce code duplication in ScopeMapper and UpdateScopeHandler
kamiazya Sep 24, 2025
c5bcb1c
docs: improve docstring coverage and reduce code duplication
kamiazya Sep 24, 2025
42e8d94
refactor(sonar): reduce cognitive complexity in error mappers
kamiazya Sep 24, 2025
a82aa16
refactor(sonar): reduce cognitive complexity in AspectValue ISO 8601 …
kamiazya Sep 24, 2025
9e2e120
refactor: reduce cognitive complexity of FilterExpressionParser.token…
kamiazya Sep 24, 2025
69b95e0
refactor: reduce cognitive complexity of AspectQueryParser.tokenizeEi…
kamiazya Sep 24, 2025
c2de3b2
refactor: reduce cognitive complexity in AspectValueValidationService…
kamiazya Sep 24, 2025
14068b5
refactor: extract duplicate string literals into constants
kamiazya Sep 24, 2025
fb6549c
refactor: extract numeric duplicate literals in CompletionCommand
kamiazya Sep 24, 2025
9404b74
refactor: consolidate identical conditional branches in ApplicationEr…
kamiazya Sep 24, 2025
d1b4eb5
Merge remote-tracking branch 'origin/main' into feature/aggregate-ref…
kamiazya Sep 25, 2025
6d69a8d
fix: E2Eイベントソーシング統合テストの期待値修正とNative Image対応
kamiazya Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions apps/scopes/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,25 @@ dependencies {
testImplementation(libs.kotlinx.coroutines.test)
}

// Add a dedicated sourceSet for Native Image (separated from JVM main)
sourceSets {
create("native") {
java.srcDir("src/native/kotlin")
resources.srcDir("src/native/resources")
}
}

// Inherit dependencies from main for the native sourceSet
configurations.named("nativeImplementation").configure {
extendsFrom(configurations.getByName("implementation"))
}
configurations.named("nativeRuntimeOnly").configure {
extendsFrom(configurations.getByName("runtimeOnly"))
}

// Use the standard entrypoint for JVM runs
application {
mainClass.set("io.github.kamiazya.scopes.apps.cli.MainKt")
mainClass.set("io.github.kamiazya.scopes.apps.cli.Main")
}

tasks.test {
Expand All @@ -90,9 +107,10 @@ tasks.test {
graalvmNative {
binaries {
named("main") {
classpath.from(sourceSets.getByName("native").runtimeClasspath)
imageName.set("scopes")
mainClass.set("io.github.kamiazya.scopes.apps.cli.MainKt")
useFatJar.set(true)
mainClass.set("io.github.kamiazya.scopes.apps.cli.nativeimage.NativeImageMain")
useFatJar.set(false)

buildArgs.addAll(
listOf(
Expand All @@ -111,9 +129,10 @@ graalvmNative {
"--exclude-config",
".*sqlite-jdbc.*\\.jar",
".*native-image.*",
"-H:ResourceConfigurationFiles=${layout.buildDirectory.get()}/resources/main/META-INF/native-image/resource-config.json",
"-H:ReflectionConfigurationFiles=${layout.buildDirectory.get()}/resources/main/META-INF/native-image/reflect-config.json",
"-H:JNIConfigurationFiles=${layout.buildDirectory.get()}/resources/main/META-INF/native-image/jni-config.json",
"-H:ResourceConfigurationFiles=${layout.buildDirectory.get()}/resources/native/META-INF/native-image/resource-config.json",
"-H:ReflectionConfigurationFiles=${layout.buildDirectory.get()}/resources/native/META-INF/native-image/reflect-config.json",
"-H:JNIConfigurationFiles=${layout.buildDirectory.get()}/resources/native/META-INF/native-image/jni-config.json",
"-H:SerializationConfigurationFiles=${layout.buildDirectory.get()}/resources/native/META-INF/native-image/serialization-config.json",
),
)
}
Expand Down Expand Up @@ -215,3 +234,9 @@ tasks.register("nativeE2eTest") {
// tasks.named("check") {
// dependsOn("nativeSmokeTest")
// }


// Avoid duplicate META-INF/native-image entries in native resources
tasks.named<org.gradle.language.jvm.tasks.ProcessResources>("processNativeResources") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import io.github.kamiazya.scopes.scopemanagement.domain.event.ScopeDescriptionUp
import io.github.kamiazya.scopes.scopemanagement.domain.event.ScopeParentChanged
import io.github.kamiazya.scopes.scopemanagement.domain.event.ScopeRestored
import io.github.kamiazya.scopes.scopemanagement.domain.event.ScopeTitleUpdated
import io.github.kamiazya.scopes.scopemanagement.domain.event.AliasAssigned
import io.github.kamiazya.scopes.scopemanagement.domain.event.AliasRemoved
import io.github.kamiazya.scopes.scopemanagement.domain.event.AliasNameChanged
import io.github.kamiazya.scopes.scopemanagement.domain.event.CanonicalAliasReplaced

/**
* Bootstrapper responsible for registering all domain event types.
Expand Down Expand Up @@ -56,6 +60,7 @@ class EventTypeRegistrar(private val eventTypeMapping: EventTypeMapping, private

private fun registerScopeManagementEvents() {
val events = listOf(
// Scope events
ScopeCreated::class,
ScopeDeleted::class,
ScopeArchived::class,
Expand All @@ -67,6 +72,11 @@ class EventTypeRegistrar(private val eventTypeMapping: EventTypeMapping, private
ScopeAspectRemoved::class,
ScopeAspectsCleared::class,
ScopeAspectsUpdated::class,
// Alias events
AliasAssigned::class,
AliasRemoved::class,
AliasNameChanged::class,
CanonicalAliasReplaced::class,
)

events.forEach { eventClass ->
Expand All @@ -81,6 +91,7 @@ class EventTypeRegistrar(private val eventTypeMapping: EventTypeMapping, private
// This allows reading events that were persisted before stable IDs were introduced
if (eventTypeMapping is DefaultEventTypeMapping) {
val legacyEvents = listOf(
// Scope events
ScopeCreated::class,
ScopeDeleted::class,
ScopeArchived::class,
Expand All @@ -92,6 +103,11 @@ class EventTypeRegistrar(private val eventTypeMapping: EventTypeMapping, private
ScopeAspectRemoved::class,
ScopeAspectsCleared::class,
ScopeAspectsUpdated::class,
// Alias events
AliasAssigned::class,
AliasRemoved::class,
AliasNameChanged::class,
CanonicalAliasReplaced::class,
)

legacyEvents.forEach { eventClass ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package io.github.kamiazya.scopes.apps.cli.di.scopemanagement

import io.github.kamiazya.scopes.contracts.eventstore.EventStoreCommandPort
import io.github.kamiazya.scopes.contracts.eventstore.EventStoreQueryPort
import io.github.kamiazya.scopes.platform.application.lifecycle.ApplicationBootstrapper
import io.github.kamiazya.scopes.platform.application.port.TransactionManager
import io.github.kamiazya.scopes.platform.domain.event.DomainEvent
import io.github.kamiazya.scopes.platform.infrastructure.transaction.SqlDelightTransactionManager
import io.github.kamiazya.scopes.platform.observability.logging.Logger
import io.github.kamiazya.scopes.platform.observability.metrics.DefaultProjectionMetrics
import io.github.kamiazya.scopes.platform.observability.metrics.InMemoryMetricsRegistry
import io.github.kamiazya.scopes.platform.observability.metrics.MetricsRegistry
import io.github.kamiazya.scopes.platform.observability.metrics.ProjectionMetrics
import io.github.kamiazya.scopes.scopemanagement.application.port.EventPublisher
import io.github.kamiazya.scopes.scopemanagement.db.ScopeManagementDatabase
import io.github.kamiazya.scopes.scopemanagement.domain.repository.ActiveContextRepository
import io.github.kamiazya.scopes.scopemanagement.domain.repository.AspectDefinitionRepository
Expand All @@ -17,13 +27,20 @@ import io.github.kamiazya.scopes.scopemanagement.infrastructure.adapters.ErrorMa
import io.github.kamiazya.scopes.scopemanagement.infrastructure.alias.generation.DefaultAliasGenerationService
import io.github.kamiazya.scopes.scopemanagement.infrastructure.alias.generation.providers.DefaultWordProvider
import io.github.kamiazya.scopes.scopemanagement.infrastructure.alias.generation.strategies.HaikunatorStrategy
import io.github.kamiazya.scopes.scopemanagement.infrastructure.bootstrap.ActiveContextBootstrap
import io.github.kamiazya.scopes.scopemanagement.infrastructure.bootstrap.AspectPresetBootstrap
import io.github.kamiazya.scopes.scopemanagement.infrastructure.factory.EventSourcingRepositoryFactory
import io.github.kamiazya.scopes.scopemanagement.infrastructure.projection.EventProjectionService
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightActiveContextRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightAspectDefinitionRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightContextViewRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightEventOutboxRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightScopeAliasRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.repository.SqlDelightScopeRepository
import io.github.kamiazya.scopes.scopemanagement.infrastructure.service.AspectQueryFilterValidator
import io.github.kamiazya.scopes.scopemanagement.infrastructure.sqldelight.SqlDelightDatabaseProvider
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.koin.core.qualifier.named
import org.koin.dsl.module

Expand Down Expand Up @@ -98,32 +115,82 @@ val scopeManagementInfrastructureModule = module {
ErrorMapper(logger = get())
}

// Metrics infrastructure
single<MetricsRegistry> { InMemoryMetricsRegistry() }
single<ProjectionMetrics> {
DefaultProjectionMetrics(metricsRegistry = get())
}

// Serializers for domain events (Scope context)
single<SerializersModule> { io.github.kamiazya.scopes.scopemanagement.infrastructure.serialization.ScopeEventSerializersModule.create() }
single<Json>(named("scopeEventJson")) {
val module: SerializersModule = get()
Json {
serializersModule = module
ignoreUnknownKeys = true
isLenient = true
classDiscriminator = "type"
}
}

// Event projector for RDB projection
single {
EventProjectionService(
scopeRepository = get(),
scopeAliasRepository = get(),
logger = get(),
projectionMetrics = get(),
)
}

// Outbox repository + projector + publisher (processImmediately to preserve current behavior)
single<SqlDelightEventOutboxRepository> { SqlDelightEventOutboxRepository(get(named("scopeManagement"))) }
single {
io.github.kamiazya.scopes.scopemanagement.infrastructure.projection.OutboxProjectionService(
outboxRepository = get(),
projectionService = get(),
json = get(named("scopeEventJson")),
logger = get(),
)
}
single<EventPublisher> {
io.github.kamiazya.scopes.scopemanagement.infrastructure.projection.OutboxEventProjectionService(
outboxRepository = get(),
projector = get(),
json = get(named("scopeEventJson")),
logger = get(),
processImmediately = true,
)
}

// Event Sourcing Repository using contracts
single<EventSourcingRepository<io.github.kamiazya.scopes.platform.domain.event.DomainEvent>> {
val eventStoreCommandPort: io.github.kamiazya.scopes.contracts.eventstore.EventStoreCommandPort = get()
val eventStoreQueryPort: io.github.kamiazya.scopes.contracts.eventstore.EventStoreQueryPort = get()
val logger: io.github.kamiazya.scopes.platform.observability.logging.Logger = get()
single<EventSourcingRepository<DomainEvent>> {
val eventStoreCommandPort: EventStoreCommandPort = get()
val eventStoreQueryPort: EventStoreQueryPort = get()
val logger: Logger = get()
val serializersModule: SerializersModule? = getOrNull()

io.github.kamiazya.scopes.scopemanagement.infrastructure.factory.EventSourcingRepositoryFactory.createContractBased(
EventSourcingRepositoryFactory.createContractBased(
eventStoreCommandPort = eventStoreCommandPort,
eventStoreQueryPort = eventStoreQueryPort,
logger = logger,
serializersModule = serializersModule,
)
}

// External Services are now provided by their own modules
// UserPreferencesService is provided by UserPreferencesModule

// Bootstrap services - registered as ApplicationBootstrapper for lifecycle management
single<io.github.kamiazya.scopes.platform.application.lifecycle.ApplicationBootstrapper>(qualifier = named("AspectPresetBootstrap")) {
io.github.kamiazya.scopes.scopemanagement.infrastructure.bootstrap.AspectPresetBootstrap(
single<ApplicationBootstrapper>(qualifier = named("AspectPresetBootstrap")) {
AspectPresetBootstrap(
aspectDefinitionRepository = get(),
logger = get(),
)
}

single<io.github.kamiazya.scopes.platform.application.lifecycle.ApplicationBootstrapper>(qualifier = named("ActiveContextBootstrap")) {
io.github.kamiazya.scopes.scopemanagement.infrastructure.bootstrap.ActiveContextBootstrap(
single<ApplicationBootstrapper>(qualifier = named("ActiveContextBootstrap")) {
ActiveContextBootstrap(
activeContextRepository = get(),
logger = get(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import io.github.kamiazya.scopes.scopemanagement.application.command.handler.con
import io.github.kamiazya.scopes.scopemanagement.application.factory.ScopeFactory
import io.github.kamiazya.scopes.scopemanagement.application.mapper.ApplicationErrorMapper
import io.github.kamiazya.scopes.scopemanagement.application.port.DomainEventPublisher
import io.github.kamiazya.scopes.scopemanagement.application.port.EventPublisher
import io.github.kamiazya.scopes.scopemanagement.application.query.handler.aspect.GetAspectDefinitionHandler
import io.github.kamiazya.scopes.scopemanagement.application.query.handler.aspect.ListAspectDefinitionsHandler
import io.github.kamiazya.scopes.scopemanagement.application.query.handler.context.GetContextViewHandler
Expand Down Expand Up @@ -119,24 +120,27 @@ val scopeManagementModule = module {
)
}

// Use Case Handlers
// Use Case Handlers - Event Sourcing
single {
CreateScopeHandler(
scopeFactory = get(),
eventSourcingRepository = get(),
scopeRepository = get(),
scopeAliasRepository = get(),
aliasGenerationService = get(),
hierarchyApplicationService = get(),
hierarchyService = get(),
transactionManager = get(),
hierarchyPolicyProvider = get(),
eventPublisher = get<io.github.kamiazya.scopes.scopemanagement.application.port.EventPublisher>(),
aliasGenerationService = get(),
applicationErrorMapper = get(),
logger = get(),
)
}

single {
UpdateScopeHandler(
eventSourcingRepository = get(),
eventPublisher = get<io.github.kamiazya.scopes.scopemanagement.application.port.EventPublisher>(),
scopeRepository = get(),
scopeAliasRepository = get(),
transactionManager = get(),
applicationErrorMapper = get(),
logger = get(),
Expand All @@ -145,7 +149,10 @@ val scopeManagementModule = module {

single {
DeleteScopeHandler(
eventSourcingRepository = get(),
eventPublisher = get<io.github.kamiazya.scopes.scopemanagement.application.port.EventPublisher>(),
scopeRepository = get(),
scopeHierarchyService = get(),
transactionManager = get(),
applicationErrorMapper = get(),
logger = get(),
Expand Down
Loading
Loading