Add backpressure stack for agentic coding quality gates

PostToolUse hooks run after every file edit:
- Backend: ./mvnw compile (Checkstyle Google Style + javac)
- Frontend: vue-tsc --noEmit + oxlint + ESLint

Stop hook runs test suites when source files changed, blocks the
agent on failure and re-engages it to fix the issue. Output is
filtered to [ERROR] lines only for context efficiency.

Static analysis: Checkstyle (validate phase), SpotBugs (verify phase),
ArchUnit (9 hexagonal architecture rules as JUnit tests).

Fail-fast: Surefire skipAfterFailureCount=1, Vitest bail=1.
Test log noise suppressed via logback-test.xml (WARN level),
redirectTestOutputToFile, and trimStackTrace.

Existing Java sources reformatted to Google Style (2-space indent,
import order, Javadoc on public types).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 02:44:15 +01:00
parent a55174b323
commit a9802c2881
15 changed files with 1098 additions and 43 deletions

View File

@@ -3,10 +3,12 @@ package de.fete;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/** Spring Boot entry point for the fete application. */
@SpringBootApplication
public class FeteApplication {
public static void main(String[] args) {
SpringApplication.run(FeteApplication.class, args);
}
/** Starts the application. */
public static void main(String[] args) {
SpringApplication.run(FeteApplication.class, args);
}
}

View File

@@ -1,15 +1,16 @@
package de.fete.adapter.in.web;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/** REST endpoint for health checks. */
@RestController
public class HealthController {
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "ok");
}
/** Returns a simple health status. */
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "ok");
}
}

View File

@@ -1,31 +1,31 @@
package de.fete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class FeteApplicationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private MockMvc mockMvc;
@Test
void contextLoads() {
// Spring context starts successfully
}
@Test
void contextLoads() {
// Spring context starts successfully
}
@Test
void healthEndpointReturns200() throws Exception {
mockMvc.perform(get("/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("ok"));
}
@Test
void healthEndpointReturns200() throws Exception {
mockMvc.perform(get("/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("ok"));
}
}

View File

@@ -0,0 +1,63 @@
package de.fete;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
class HexagonalArchitectureTest {
@ArchTest
static final ArchRule onionArchitectureIsRespected = onionArchitecture()
.domainModels("de.fete.domain.model..")
.domainServices("de.fete.domain.port.in..", "de.fete.domain.port.out..")
.applicationServices("de.fete.application.service..")
.adapter("web", "de.fete.adapter.in.web..")
.adapter("persistence", "de.fete.adapter.out.persistence..")
.adapter("config", "de.fete.config..");
@ArchTest
static final ArchRule domainMustNotDependOnAdapters = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter..");
@ArchTest
static final ArchRule domainMustNotDependOnApplication = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.application..");
@ArchTest
static final ArchRule domainMustNotDependOnConfig = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.config..");
@ArchTest
static final ArchRule inboundPortsMustBeInterfaces = classes()
.that().resideInAPackage("de.fete.domain.port.in..")
.should().beInterfaces();
@ArchTest
static final ArchRule outboundPortsMustBeInterfaces = classes()
.that().resideInAPackage("de.fete.domain.port.out..")
.should().beInterfaces();
@ArchTest
static final ArchRule domainMustNotUseSpring = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("org.springframework..");
@ArchTest
static final ArchRule webMustNotDependOnPersistence = noClasses()
.that().resideInAPackage("de.fete.adapter.in.web..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.out.persistence..");
@ArchTest
static final ArchRule persistenceMustNotDependOnWeb = noClasses()
.that().resideInAPackage("de.fete.adapter.out.persistence..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.in.web..");
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>