Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This guide defines the architectural and code quality standards for this reposit

## 2. Concurrency & State Safety
- **Anti-Pattern**: Avoid global state or uncontrolled shared-mutable state.
- **Immutability & Snapshotting**: Always prefer immutable data structures (Kotlin `data class`). Use the `.copy()` pattern for all state transitions to ensure atomic, thread-safe updates and clear history tracking.
- **Optimistic Locking**: Use `@Version` (JPA) for any high-contention resource state changes to prevent "Lost Updates."
- **Transactional Atomicity**: Business operations that span multiple modifications must be wrapped in a single transaction.

Expand All @@ -17,12 +18,17 @@ This guide defines the architectural and code quality standards for this reposit
- **Audit Trails**: Capture meaningful state transitions (e.g., creation, updates) rather than just overriding fields.
- **Fail Fast**: Implement strict validation on all incoming requests before they reach the service layer.

## 4. Operational Excellence
## 4. Workflow & Automation
- **Format Before Finishing**: Always run `./gradlew ktlintFormat` as the final step of any implementation task.
- **Verify Before Pushing**: Ensure all tasks pass `./gradlew check` to verify both formatting and tests.
- **Editor Synchronization**: Use the project's `.editorconfig` to ensure local IDEs match the CI linting rules.

## 5. Operational Excellence
- **Observability**: Instrument key business events using metrics (e.g., Micrometer) and structured logging.
- **Clean Commits**: Use short, lowercase prefixes: `feat:`, `fix:`, `test:`, `arch:`, `sec:`, or `docs:`.
- **Shift-Left Security**: Ensure high-severity dependencies are flagged in CI/CD.

## 5. Implementation Workflow
## 6. Implementation Workflow
1. **Red-Green-Refactor**: Prioritize TDD for complex domain logic.
2. **ArchUnit Alignment**: Verify that new dependencies don't violate layer isolation.
3. **Proactive Verification**: Run `./gradlew test ktlintCheck` before finalizing any change.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{kt,kts}]
ktlint_code_style = ktlint_official
max_line_length = 120
insert_final_newline = true
78 changes: 45 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,46 @@ Stardust Station serves as a neutral hub for various interstellar fleets. The pr

## Engineering Stack

- **Language**: Kotlin 1.9.25 (Expressive, null-safe, and concise)
- **Framework**: Spring Boot 3.4.1
- **Data Layer**: Spring Data JPA with H2 (In-memory feedback loops)
- **Language**: Kotlin 2.1.20 (Expressive, null-safe, and AI-native)
- **Framework**: Spring Boot 3.4.3
- **AI/MCP**: Spring AI MCP (Model Context Protocol) ready
- **Data Layer**: Spring Data JPA with H2 (In-memory persistence)
- **Observability**: Spring Actuator & Micrometer (Prometheus metrics)
- **Testing**: JUnit 5, MockK, and ArchUnit (Behavior & Architecture verification)
- **Quality**: ktlint for automated style enforcement
- **Testing**: JUnit 5, MockK, and ArchUnit
- **Quality**: ktlint 14.0.1 for automated style enforcement

## Quick Start

### Prerequisites
- Java 21+

### Local Development
```bash
# Clone the repository
git clone <repository-url>
cd stardust-station

# Launch the station control locally
./gradlew bootRun
```

The service will be available at `http://localhost:8080`.

## Development Standards

Before pushing any code, ensure it meets the station's quality and style requirements:

1. **Format Code**: Run the auto-formatter to maintain consistency.
```bash
./gradlew ktlintFormat
```
2. **Verify Quality**: Run the lint check and all unit tests.
```bash
./gradlew check
```

## Documentation
- [Architecture Decision Records (ADRs)](docs/adr/)

## System Architecture & Patterns

Expand Down Expand Up @@ -48,33 +82,11 @@ graph TD
Web -- maps to --> Response
```

- **Domain Layer**: The heart of the application containing pure business logic (`DockingService`) and entity models. It remains agnostic of the database or web framework.
- **Port Layer**: Interface definitions for out-bound communication (e.g., `DockingBayRepository`).
- **Infrastructure Layer**: Technical implementations of the ports (JPA/Hibernate) and entry points for the application (REST Controllers).
- **DTO Pattern**: We use Data Transfer Objects to ensure that internal persistence models never leak to the API.

## Getting Started

### Prerequisites
Construction and execution require **JDK 21**.

### Setup
```bash
# Clone the repository
git clone <repository-url>
cd stardust-station

# Execute the full test suite
./gradlew test

# Launch the station control locally
./gradlew bootRun
```
### Why Hexagonal Architecture?
Hexagonal (also known as Ports and Adapters) is the ideal architectural pattern for Stardust Station because:

## Logic Snapshot: `requestDocking`
1. **Inversion of Control**: The core Domain logic (the "Inside") defines its dependencies via Interfaces (Ports). Technical implementation details (the "Outside") like H2 or REST are kept separate.
2. **Testability**: Business logic can be tested in complete isolation without starting a heavy Spring Context or Database.

The core logic handles the following protocol check-list before authorizing docking:
- **Existence**: Do the docking bay and starship registry exist?
- **Occupancy**: Is the docking bay currently free?
- **Protocol**: Does the starship's fleet affiliation match the bay's required protocol?
- **Concurrency**: Did another sensor update this bay while we were processing? (Managed by JPA Versioning)
---
*Stardust Station: Integrity through Architecture.*
58 changes: 34 additions & 24 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,52 +1,62 @@
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
kotlin("plugin.jpa") version "1.9.25"
id("org.springframework.boot") version "3.4.1"
kotlin("jvm") version "2.1.20"
kotlin("plugin.spring") version "2.1.20"
kotlin("plugin.jpa") version "2.1.20"
kotlin("plugin.serialization") version "2.1.20"
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
id("org.jlleitschuh.gradle.ktlint") version "14.0.1"
}

group = "com.example.stardust"
group = "com.stardust"
version = "0.0.1-SNAPSHOT"

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}

repositories {
mavenCentral()
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")

// Spring AI MCP
implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc:1.1.2")
implementation("org.springframework.ai:spring-ai-mcp:1.1.2")

// Resilience
implementation("io.github.resilience4j:resilience4j-spring-boot3:2.3.0")

// Tools & Observability (Kept from existing)
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("io.micrometer:micrometer-registry-prometheus")

// Database
runtimeOnly("com.h2database:h2")

testImplementation("org.springframework.boot:spring-boot-starter-test") {
// Exclude Mockito to ensure we only use MockK
exclude(group = "org.mockito", module = "mockito-core")
}

// Required for Gradle to detect and run JUnit 5 tests
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

testImplementation("io.mockk:mockk:1.13.12")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("io.mockk:mockk:1.14.9")
testImplementation("com.ninja-squad:springmockk:4.0.2")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
jvmToolchain(21)
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "21"
}
}

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.stardust
package com.stardust

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stardust.domain.exception

class FleetMismatchException(
message: String,
) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.stardust.domain.model
package com.stardust.domain.model

import jakarta.persistence.Column
import jakarta.persistence.Entity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.stardust.domain.model
package com.stardust.domain.model

import jakarta.persistence.Entity
import jakarta.persistence.FetchType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.stardust.domain.model
package com.stardust.domain.model

import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.example.stardust.domain.port
package com.stardust.domain.port

import com.example.stardust.domain.model.DockingBay
import com.example.stardust.domain.model.DockingManifest
import com.example.stardust.domain.model.Starship
import com.stardust.domain.model.DockingBay
import com.stardust.domain.model.DockingManifest
import com.stardust.domain.model.Starship
import java.util.Optional

interface DockingBayRepository {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.example.stardust.domain.service
package com.stardust.domain.service

import com.example.stardust.domain.exception.FleetMismatchException
import com.example.stardust.domain.model.DockingBay
import com.example.stardust.domain.model.DockingManifest
import com.example.stardust.domain.port.DockingBayRepository
import com.example.stardust.domain.port.DockingManifestRepository
import com.example.stardust.domain.port.StarshipRepository
import com.stardust.domain.exception.FleetMismatchException
import com.stardust.domain.model.DockingBay
import com.stardust.domain.model.DockingManifest
import com.stardust.domain.port.DockingBayRepository
import com.stardust.domain.port.DockingManifestRepository
import com.stardust.domain.port.StarshipRepository
import io.micrometer.core.instrument.MeterRegistry
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand Down Expand Up @@ -68,13 +68,10 @@ class DockingService(
manifestRepository.delete(manifest)
}

fun getActiveDocks(): List<DockingBay> {
return bayRepository.findAllByIsOccupiedTrue()
}
fun getActiveDocks(): List<DockingBay> = bayRepository.findAllByIsOccupiedTrue()

fun getManifest(manifestId: Long): DockingManifest {
return manifestRepository.findById(manifestId).orElseThrow {
fun getManifest(manifestId: Long): DockingManifest =
manifestRepository.findById(manifestId).orElseThrow {
RuntimeException("Manifest not found")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.stardust.dto
package com.stardust.dto

data class DockingBayResponse(
val id: Long,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.stardust.infrastructure.persistence.jpa

import com.stardust.domain.model.DockingBay
import com.stardust.domain.model.DockingManifest
import com.stardust.domain.model.Starship
import com.stardust.domain.port.DockingBayRepository
import com.stardust.domain.port.DockingManifestRepository
import com.stardust.domain.port.StarshipRepository
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface JpaDockingBayRepository :
JpaRepository<DockingBay, Long>,
DockingBayRepository

@Repository
interface JpaStarshipRepository :
JpaRepository<Starship, Long>,
StarshipRepository

@Repository
interface JpaDockingManifestRepository :
JpaRepository<DockingManifest, Long>,
DockingManifestRepository
Loading