diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c8887b4d..c6738c34 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "ecc", "source": "./", - "description": "Harness-native ECC operator layer - 63 agents, 251 skills, 79 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", + "description": "Harness-native ECC operator layer - 64 agents, 255 skills, 79 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", "version": "2.0.0-rc.1", "author": { "name": "Affaan Mustafa", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 42db836c..de4583c4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", "version": "2.0.0-rc.1", - "description": "Harness-native ECC plugin for engineering teams - 63 agents, 251 skills, 79 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", + "description": "Harness-native ECC plugin for engineering teams - 64 agents, 255 skills, 79 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", "author": { "name": "Affaan Mustafa", "url": "https://x.com/affaanmustafa" diff --git a/AGENTS.md b/AGENTS.md index fdad0ae2..86b444d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 63 specialized agents, 251 skills, 79 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 64 specialized agents, 255 skills, 79 commands, and automated hook workflows for software development. **Version:** 2.0.0-rc.1 @@ -149,8 +149,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ## Project Structure ``` -agents/ — 63 specialized subagents -skills/ — 251 workflow skills and domain knowledge +agents/ — 64 specialized subagents +skills/ — 255 workflow skills and domain knowledge commands/ — 79 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index 2883c76e..cd5c2a78 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ This repo is the raw code only. The guides explain everything. ### v2.0.0-rc.1 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026) - **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar. -- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 63 agents, 251 skills, and 79 legacy command shims. +- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 64 agents, 255 skills, and 79 legacy command shims. - **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane. - **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system. - **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone. @@ -394,7 +394,7 @@ If you stacked methods, clean up in this order: /plugin list ecc@ecc ``` -**That's it!** You now have access to 63 agents, 251 skills, and 79 legacy command shims. +**That's it!** You now have access to 64 agents, 255 skills, and 79 legacy command shims. ### Dashboard GUI @@ -487,7 +487,6 @@ export ECC_SESSION_RETENTION_DAYS=14 export ECC_CONTEXT_MONITOR_COST_WARNINGS=off ``` - Windows PowerShell: ```powershell @@ -525,7 +524,7 @@ ECC/ | |-- plugin.json # Plugin metadata and component paths | |-- marketplace.json # Marketplace catalog for /plugin marketplace add | -|-- agents/ # 63 specialized subagents for delegation +|-- agents/ # 64 specialized subagents for delegation | |-- planner.md # Feature implementation planning | |-- architect.md # System design decisions | |-- tdd-guide.md # Test-driven development @@ -1472,9 +1471,9 @@ The configuration is automatically detected from `.opencode/opencode.json`. | Feature | Claude Code | OpenCode | Status | |---------|---------------------|----------|--------| -| Agents | PASS: 63 agents | PASS: 12 agents | **Claude Code leads** | +| Agents | PASS: 64 agents | PASS: 12 agents | **Claude Code leads** | | Commands | PASS: 79 commands | PASS: 35 commands | **Claude Code leads** | -| Skills | PASS: 251 skills | PASS: 37 skills | **Claude Code leads** | +| Skills | PASS: 255 skills | PASS: 37 skills | **Claude Code leads** | | Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** | | Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** | | MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** | @@ -1634,9 +1633,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e | Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | GitHub Copilot | |---------|-----------------------|------------|-----------|----------|----------------| -| **Agents** | 63 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A | +| **Agents** | 64 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A | | **Commands** | 79 | Shared | Instruction-based | 35 | 6 prompts | -| **Skills** | 251 | Shared | 10 (native format) | 37 | Via instructions | +| **Skills** | 255 | Shared | 10 (native format) | 37 | Via instructions | | **Hook Events** | 8 types | 15 types | None yet | 11 types | None | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | N/A | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | 1 always-on file | diff --git a/README.zh-CN.md b/README.zh-CN.md index 0cd0ea8a..d0069de0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 63 个代理、251 个技能和 79 个命令。 +**完成!** 你现在可以使用 64 个代理、255 个技能和 79 个命令。 ### multi-* 命令需要额外配置 diff --git a/agents/php-reviewer.md b/agents/php-reviewer.md index 19e62d16..0c4d31d9 100644 --- a/agents/php-reviewer.md +++ b/agents/php-reviewer.md @@ -96,7 +96,7 @@ Fix: What to change ## Framework Checks - **Laravel**: N+1 via `with()`/`load()`, `$fillable`/`$casts`, FormRequest validation, route model binding, `Gate`/`Policy` authorization, Sanctum token abilities, queue idempotency -- **Livewire**: Proper `#[Rule]` attributes, authorization in ` authorize()`, wire:model security +- **Livewire**: Proper `#[Rule]` attributes, authorization in `authorize()`, wire:model security - **Filament**: Form/table authorization, `canAccess()`, policy registration - **Plain PHP**: PDO prepared statements, password_hash/password_verify, header-based CSRF diff --git a/docs/es/skills/quarkus-patterns/SKILL.md b/docs/es/skills/quarkus-patterns/SKILL.md index fc49661a..24fc983e 100644 --- a/docs/es/skills/quarkus-patterns/SKILL.md +++ b/docs/es/skills/quarkus-patterns/SKILL.md @@ -70,18 +70,18 @@ public class OrderProcessingService { ```java @ApplicationScoped public class ProcessingService { - + public void processDocument(Document doc) { LogContext logContext = CustomLog.getCurrentContext(); try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { logContext.put("documentId", doc.getId().toString()); logContext.put("documentType", doc.getType()); logContext.put("userId", SecurityContext.getUserId()); - + log.info("Iniciando procesamiento de documento"); - + processInternal(doc); - + log.info("Procesamiento de documento completado"); } catch (Exception e) { log.error("Error en el procesamiento de documento", e); @@ -101,7 +101,7 @@ public class ProcessingService { true - + @@ -118,7 +118,7 @@ public class ProcessingService { public class EventService { private final EventRepository eventRepository; private final ObjectMapper objectMapper; - + public void createSuccessEvent(Object payload, String eventType) { Objects.requireNonNull(payload, "El payload no puede ser null"); Event event = new Event(); @@ -126,11 +126,11 @@ public class EventService { event.setStatus(EventStatus.SUCCESS); event.setPayload(serializePayload(payload)); event.setTimestamp(Instant.now()); - + eventRepository.persist(event); log.info("Evento de éxito creado: {}", eventType); } - + public void createErrorEvent(Object payload, String eventType, String errorMessage) { Objects.requireNonNull(payload, "El payload no puede ser null"); if (errorMessage == null || errorMessage.isBlank()) { @@ -142,11 +142,11 @@ public class EventService { event.setErrorMessage(errorMessage); event.setPayload(serializePayload(payload)); event.setTimestamp(Instant.now()); - + eventRepository.persist(event); log.error("Evento de error creado: {} - {}", eventType, errorMessage); } - + private String serializePayload(Object payload) { try { return objectMapper.writeValueAsString(payload); @@ -165,10 +165,10 @@ public class EventService { @RequiredArgsConstructor public class BusinessRulesPublisher { private final ProducerTemplate producerTemplate; - + public void publishSync(BusinessRulesPayload payload) { producerTemplate.sendBody( - "direct:business-rules-publisher", + "direct:business-rules-publisher", payload ); } @@ -180,23 +180,23 @@ public class BusinessRulesPublisher { ```java @ApplicationScoped public class BusinessRulesRoute extends RouteBuilder { - + @ConfigProperty(name = "camel.rabbitmq.queue.business-rules") String businessRulesQueue; - + @ConfigProperty(name = "rabbitmq.host") String rabbitHost; - + @ConfigProperty(name = "rabbitmq.port") Integer rabbitPort; - + @Override public void configure() { from("direct:business-rules-publisher") .routeId("business-rules-publisher") .log("Publicando mensaje en RabbitMQ: ${body}") .marshal().json(JsonLibrary.Jackson) - .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", + .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", businessRulesQueue, rabbitHost, rabbitPort); } } @@ -207,14 +207,14 @@ public class BusinessRulesRoute extends RouteBuilder { ```java @ApplicationScoped public class DocumentProcessingRoute extends RouteBuilder { - + @Override public void configure() { onException(ValidationException.class) .handled(true) .to("direct:validation-error-handler") .log("Error de validación: ${exception.message}"); - + from("direct:process-document") .routeId("document-processing") .log("Procesando documento: ${header.documentId}") @@ -237,19 +237,19 @@ public class DocumentProcessingRoute extends RouteBuilder { ```java @ApplicationScoped public class FileMonitoringRoute extends RouteBuilder { - + @ConfigProperty(name = "file.input.directory") String inputDirectory; - + @ConfigProperty(name = "file.processed.directory") String processedDirectory; - + @ConfigProperty(name = "file.error.directory") String errorDirectory; - + @Override public void configure() { - from("file:" + inputDirectory + "?move=" + processedDirectory + + from("file:" + inputDirectory + "?move=" + processedDirectory + "&moveFailed=" + errorDirectory + "&delay=5000") .routeId("file-monitor") .log("Procesando archivo: ${header.CamelFileName}") @@ -302,7 +302,7 @@ public class DocumentResource { ```java @ApplicationScoped public class DocumentRepository implements PanacheRepository { - + public List findByStatus(DocumentStatus status, int page, int size) { return find("status = ?1 order by createdAt desc", status) .page(page, size) @@ -331,11 +331,11 @@ public class DocumentService { document.setDescription(request.description()); document.setStatus(DocumentStatus.PENDING); document.setCreatedAt(Instant.now()); - + repo.persist(document); - + eventService.createSuccessEvent(document, "DOCUMENT_CREATED"); - + return document; } } @@ -352,7 +352,7 @@ public record CreateDocumentRequest( public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) { public static DocumentResponse from(Document document) { - return new DocumentResponse(document.getId(), document.getReferenceNumber(), + return new DocumentResponse(document.getId(), document.getReferenceNumber(), document.getStatus()); } } @@ -368,7 +368,7 @@ public class ValidationExceptionMapper implements ExceptionMapper cv.getPropertyPath() + ": " + cv.getMessage()) .collect(Collectors.joining(", ")); - + return Response.status(Response.Status.BAD_REQUEST) .entity(Map.of("error", "validation_error", "message", message)) .build(); @@ -385,25 +385,25 @@ public class ValidationExceptionMapper implements ExceptionMapper uploadOriginalFile( - InputStream inputStream, - long size, + InputStream inputStream, + long size, LogContext logContext, InvoiceFormat format) { - + return CompletableFuture.supplyAsync(() -> { try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { String path = generateStoragePath(format); - + PutObjectRequest request = PutObjectRequest.builder() .bucket(bucketName) .key(path) .contentLength(size) .build(); - + s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size)); - + return new StoredDocumentInfo(path, size, Instant.now()); } catch (Exception e) { log.error("Error al subir archivo a S3", e); diff --git a/docs/es/skills/quarkus-security/SKILL.md b/docs/es/skills/quarkus-security/SKILL.md index 7fbebdd9..c7e4743b 100644 --- a/docs/es/skills/quarkus-security/SKILL.md +++ b/docs/es/skills/quarkus-security/SKILL.md @@ -28,7 +28,7 @@ Buenas prácticas para asegurar aplicaciones Quarkus con autenticación, autoriz @Path("/api/protected") @Authenticated public class ProtectedResource { - + @Inject JsonWebToken jwt; @@ -65,19 +65,19 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET} @Provider @Priority(Priorities.AUTHENTICATION) public class CustomAuthFilter implements ContainerRequestFilter { - + @Inject SecurityIdentity identity; @Override public void filter(ContainerRequestContext requestContext) { String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - + if (authHeader == null || !authHeader.startsWith("Bearer ")) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); return; } - + String token = authHeader.substring(7); if (!validateToken(token)) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); @@ -98,7 +98,7 @@ public class CustomAuthFilter implements ContainerRequestFilter { @Path("/api/admin") @RolesAllowed("ADMIN") public class AdminResource { - + @GET @Path("/users") public List listUsers() { @@ -116,7 +116,7 @@ public class AdminResource { @Path("/api/users") public class UserResource { - + @Inject SecurityIdentity securityIdentity; @@ -124,7 +124,7 @@ public class UserResource { @Path("/{id}") @RolesAllowed("USER") public Response getUser(@PathParam("id") Long id) { - if (!securityIdentity.hasRole("ADMIN") && + if (!securityIdentity.hasRole("ADMIN") && !isOwner(id, securityIdentity.getPrincipal().getName())) { return Response.status(Response.Status.FORBIDDEN).build(); } @@ -138,7 +138,7 @@ public class UserResource { ```java @ApplicationScoped public class SecurityService { - + @Inject SecurityIdentity securityIdentity; @@ -146,7 +146,7 @@ public class SecurityService { if (securityIdentity.isAnonymous()) { return false; } - + if (securityIdentity.hasRole("ADMIN")) { return true; } @@ -216,7 +216,7 @@ List users = User.list("email = ?1 and active = ?2", email, true); Optional user = User.find("username", username).firstResultOptional(); // BIEN: Parámetros nombrados -List users = User.list("email = :email and age > :minAge", +List users = User.list("email = :email and age > :minAge", Parameters.with("email", email).and("minAge", 18)); ``` @@ -243,7 +243,7 @@ public class User extends PanacheEntity { ```java @ApplicationScoped public class PasswordService { - + public String hash(String plainPassword) { return BcryptUtil.bcryptHash(plainPassword); } @@ -297,7 +297,7 @@ public class RateLimitFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) { String clientId = getClientIdentifier(); - RateLimiter limiter = limiters.computeIfAbsent(clientId, + RateLimiter limiter = limiters.computeIfAbsent(clientId, k -> RateLimiter.create(100.0)); // 100 solicitudes por segundo if (!limiter.tryAcquire()) { @@ -324,17 +324,17 @@ public class RateLimitFilter implements ContainerRequestFilter { ```java @Provider public class SecurityHeadersFilter implements ContainerResponseFilter { - + @Override public void filter(ContainerRequestContext request, ContainerResponseContext response) { MultivaluedMap headers = response.getHeaders(); - + headers.putSingle("X-Frame-Options", "DENY"); headers.putSingle("X-Content-Type-Options", "nosniff"); headers.putSingle("X-XSS-Protection", "1; mode=block"); headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); // CSP: evitar 'unsafe-inline' para script-src; usar nonces o hashes - headers.putSingle("Content-Security-Policy", + headers.putSingle("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"); } } @@ -351,11 +351,11 @@ public class AuditService { SecurityIdentity securityIdentity; public void logAccess(String resource, String action) { - String user = securityIdentity.isAnonymous() - ? "anonymous" + String user = securityIdentity.isAnonymous() + ? "anonymous" : securityIdentity.getPrincipal().getName(); - - LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", + + LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", user, action, resource, Instant.now()); } } diff --git a/docs/es/skills/quarkus-tdd/SKILL.md b/docs/es/skills/quarkus-tdd/SKILL.md index b143f077..622807d1 100644 --- a/docs/es/skills/quarkus-tdd/SKILL.md +++ b/docs/es/skills/quarkus-tdd/SKILL.md @@ -32,19 +32,19 @@ Orientación TDD para servicios Quarkus 3.x con 80%+ de cobertura (unit + integr @ExtendWith(MockitoExtension.class) @DisplayName("Pruebas Unitarias de OrderService") class OrderServiceTest { - + @Mock private OrderRepository orderRepository; - + @Mock private EventService eventService; - + @Mock private FulfillmentPublisher fulfillmentPublisher; - + @InjectMocks private OrderService orderService; - + private CreateOrderCommand validCommand; @BeforeEach @@ -58,16 +58,16 @@ class OrderServiceTest { @Nested @DisplayName("Pruebas para createOrder") class CreateOrder { - + @Test @DisplayName("Debe persistir orden y publicar evento de fulfillment") void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() { // ARRANGE doNothing().when(orderRepository).persist(any(Order.class)); - + // ACT OrderReceipt receipt = orderService.createOrder(validCommand); - + // ASSERT assertThat(receipt).isNotNull(); assertThat(receipt.customerId()).isEqualTo("customer-123"); @@ -81,7 +81,7 @@ class OrderServiceTest { void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() { // ARRANGE CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines()); - + // ACT & ASSERT WebApplicationException exception = assertThrows( WebApplicationException.class, @@ -99,13 +99,13 @@ class OrderServiceTest { // ARRANGE doThrow(new PersistenceException("base de datos no disponible")) .when(orderRepository).persist(any(Order.class)); - + // ACT & ASSERT PersistenceException exception = assertThrows( PersistenceException.class, () -> orderService.createOrder(validCommand) ); - + assertThat(exception.getMessage()).contains("base de datos no disponible"); verify(eventService).createErrorEvent( eq(validCommand), @@ -168,20 +168,20 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); mockRabbitMQ.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.replaceFromWith("direct:business-rules-publisher"); advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT mockRabbitMQ.assertIsSatisfied(5000); - + assertThat(mockRabbitMQ.getExchanges()).hasSize(1); String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); @@ -199,17 +199,17 @@ class EventServiceTest { @Mock private EventRepository eventRepository; - + @Mock private ObjectMapper objectMapper; - + @InjectMocks private EventService eventService; @Nested @DisplayName("Pruebas para createSuccessEvent") class CreateSuccessEvent { - + @Test @DisplayName("Debe crear evento de éxito con atributos correctos") void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { @@ -217,13 +217,13 @@ class EventServiceTest { BusinessRulesPayload testPayload = new BusinessRulesPayload(); testPayload.setDocumentId(1L); when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("DOCUMENT_PROCESSED") && event.getStatus() == EventStatus.SUCCESS && event.getTimestamp() != null @@ -235,13 +235,13 @@ class EventServiceTest { void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { // ARRANGE Object nullPayload = null; - + // ACT & ASSERT NullPointerException exception = assertThrows( NullPointerException.class, () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") ); - + assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); verify(eventRepository, never()).persist(any()); } @@ -250,7 +250,7 @@ class EventServiceTest { @Nested @DisplayName("Pruebas para createErrorEvent") class CreateErrorEvent { - + @ParameterizedTest @DisplayName("Debe rechazar mensajes de error inválidos") @ValueSource(strings = {"", " "}) @@ -263,7 +263,7 @@ class EventServiceTest { IllegalArgumentException.class, () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) ); - + assertThat(exception.getMessage()).contains("Error message cannot be blank"); } } @@ -278,10 +278,10 @@ class FileStorageServiceTest { @Mock private S3Client s3Client; - + @Mock private ExecutorService executorService; - + @InjectMocks private FileStorageService fileStorageService; @@ -293,15 +293,15 @@ class FileStorageServiceTest { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenThrow(new StorageException("S3 no disponible")); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + // ASSERT assertThatThrownBy(() -> future.join()) .isInstanceOf(CompletionException.class) diff --git a/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md b/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md index 4c418da1..33171035 100644 --- a/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md +++ b/docs/fixes/HOOK-FIX-20260421-ADDENDUM.md @@ -17,7 +17,7 @@ Node.js の `child_process.spawn` で `.sh` ファイルを直接実行すると **EFTYPE** で失敗する: ```js -spawn('C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh', +spawn('C:/Users/sugig/.claude/skills/continuous-learning/hooks/observe-wrapper.sh', ['post'], {stdio:['pipe','pipe','pipe']}); // → Error: spawn EFTYPE (errno -4028) ``` diff --git a/docs/ja-JP/skills/agent-payment-x402/SKILL.md b/docs/ja-JP/skills/agent-payment-x402/SKILL.md index 80c3519f..20a09d91 100644 --- a/docs/ja-JP/skills/agent-payment-x402/SKILL.md +++ b/docs/ja-JP/skills/agent-payment-x402/SKILL.md @@ -119,7 +119,7 @@ async function main() { } // stdio トランスポートを介して agentpay MCP サーバーに接続する。 - // サーバーが必要とする env 変数のみをホワイトリストに登録する — + // サーバーが必要とする env 変数のみをホワイトリストに登録する — // 秘密鍵を管理するサードパーティのサブプロセスに process.env のすべてを渡さない。 const transport = new StdioClientTransport({ command: "npx", diff --git a/docs/ja-JP/skills/homelab-pihole-dns/SKILL.md b/docs/ja-JP/skills/homelab-pihole-dns/SKILL.md index 22577b09..02227fd1 100644 --- a/docs/ja-JP/skills/homelab-pihole-dns/SKILL.md +++ b/docs/ja-JP/skills/homelab-pihole-dns/SKILL.md @@ -96,11 +96,11 @@ less pi-hole-install.sh # review before proceeding bash pi-hole-install.sh # Follow the interactive installer: -# 1. Select network interface (eth0 for wired — recommended) -# 2. Select upstream DNS (Cloudflare or leave default — can change later) -# 3. Confirm static IP -# 4. Install the web admin interface (recommended) -# 5. Note the admin password shown at the end +# 1. Select network interface (eth0 for wired — recommended) +# 2. Select upstream DNS (Cloudflare or leave default — can change later) +# 3. Confirm static IP +# 4. Install the web admin interface (recommended) +# 5. Note the admin password shown at the end ``` ## Pointing Your Network at Pi-hole @@ -183,9 +183,9 @@ sudo systemctl start cloudflared sudo systemctl enable cloudflared # Now point Pi-hole at the local DoH proxy: -# Pi-hole admin → Settings → DNS → Custom upstream DNS -# Set to: 127.0.0.1#5053 -# Uncheck all other upstream resolvers +# Pi-hole admin → Settings → DNS → Custom upstream DNS +# Set to: 127.0.0.1#5053 +# Uncheck all other upstream resolvers ``` ## Local DNS Records diff --git a/docs/ja-JP/skills/homelab-wireguard-vpn/SKILL.md b/docs/ja-JP/skills/homelab-wireguard-vpn/SKILL.md index b95f291d..c37b1df4 100644 --- a/docs/ja-JP/skills/homelab-wireguard-vpn/SKILL.md +++ b/docs/ja-JP/skills/homelab-wireguard-vpn/SKILL.md @@ -218,8 +218,8 @@ stays reachable after an IP change. restart: unless-stopped # ddns.env (chmod 600, not committed to git): -# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id -# SETTINGS_CLOUDFLARE_TOKEN=your_api_token +# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id +# SETTINGS_CLOUDFLARE_TOKEN=your_api_token # Option 2: DuckDNS (free, simple) Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org) diff --git a/docs/ja-JP/skills/quarkus-security/SKILL.md b/docs/ja-JP/skills/quarkus-security/SKILL.md index 1721d0ff..854f2fde 100644 --- a/docs/ja-JP/skills/quarkus-security/SKILL.md +++ b/docs/ja-JP/skills/quarkus-security/SKILL.md @@ -28,7 +28,7 @@ origin: ECC @Path("/api/protected") @Authenticated public class ProtectedResource { - + @Inject JsonWebToken jwt; @@ -65,20 +65,20 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET} @Provider @Priority(Priorities.AUTHENTICATION) public class CustomAuthFilter implements ContainerRequestFilter { - + @Inject SecurityIdentity identity; @Override public void filter(ContainerRequestContext requestContext) { String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - + // ヘッダーが無いまたは不正形式の場合は即座に拒否 if (authHeader == null || !authHeader.startsWith("Bearer ")) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); return; } - + String token = authHeader.substring(7); if (!validateToken(token)) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); @@ -100,7 +100,7 @@ public class CustomAuthFilter implements ContainerRequestFilter { @Path("/api/admin") @RolesAllowed("ADMIN") public class AdminResource { - + @GET @Path("/users") public List listUsers() { @@ -118,7 +118,7 @@ public class AdminResource { @Path("/api/users") public class UserResource { - + @Inject SecurityIdentity securityIdentity; @@ -127,7 +127,7 @@ public class UserResource { @RolesAllowed("USER") public Response getUser(@PathParam("id") Long id) { // 所有権確認 - if (!securityIdentity.hasRole("ADMIN") && + if (!securityIdentity.hasRole("ADMIN") && !isOwner(id, securityIdentity.getPrincipal().getName())) { return Response.status(Response.Status.FORBIDDEN).build(); } @@ -145,7 +145,7 @@ public class UserResource { ```java @ApplicationScoped public class SecurityService { - + @Inject SecurityIdentity securityIdentity; @@ -153,7 +153,7 @@ public class SecurityService { if (securityIdentity.isAnonymous()) { return false; } - + if (securityIdentity.hasRole("ADMIN")) { return true; } @@ -229,7 +229,7 @@ List users = User.list("email = ?1 and active = ?2", email, true); Optional user = User.find("username", username).firstResultOptional(); // 良い例:名前付きパラメータ -List users = User.list("email = :email and age > :minAge", +List users = User.list("email = :email and age > :minAge", Parameters.with("email", email).and("minAge", 18)); ``` @@ -256,7 +256,7 @@ public class User extends PanacheEntity { ```java @ApplicationScoped public class PasswordService { - + public String hash(String plainPassword) { return BcryptUtil.bcryptHash(plainPassword); } @@ -324,7 +324,7 @@ quarkus.vault.authentication.kubernetes.role=my-role ```java @ApplicationScoped public class SecretService { - + @ConfigProperty(name = "api-key") String apiKey; // Vault から取得 @@ -350,7 +350,7 @@ public class RateLimitFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) { String clientId = getClientIdentifier(); - RateLimiter limiter = limiters.computeIfAbsent(clientId, + RateLimiter limiter = limiters.computeIfAbsent(clientId, k -> RateLimiter.create(100.0)); // 1秒あたり100リクエスト if (!limiter.tryAcquire()) { @@ -376,25 +376,25 @@ public class RateLimitFilter implements ContainerRequestFilter { ```java @Provider public class SecurityHeadersFilter implements ContainerResponseFilter { - + @Override public void filter(ContainerRequestContext request, ContainerResponseContext response) { MultivaluedMap headers = response.getHeaders(); - + // クリックジャッキング防止 headers.putSingle("X-Frame-Options", "DENY"); - + // XSS保護 headers.putSingle("X-Content-Type-Options", "nosniff"); headers.putSingle("X-XSS-Protection", "1; mode=block"); - + // HSTS headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - + // CSP — script-src用の'unsafe-inline'は避けてください。XSS保護を無効化します。 // 代わりにnoncesまたはhashesを使用します。CSSフレームワークが必要な場合、 // style-srcの'unsafe-inline'は許容ですが、可能な場合はnoncesを優先してください。 - headers.putSingle("Content-Security-Policy", + headers.putSingle("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"); } } @@ -411,11 +411,11 @@ public class AuditService { SecurityIdentity securityIdentity; public void logAccess(String resource, String action) { - String user = securityIdentity.isAnonymous() - ? "anonymous" + String user = securityIdentity.isAnonymous() + ? "anonymous" : securityIdentity.getPrincipal().getName(); - - LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", + + LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", user, action, resource, Instant.now()); } } diff --git a/docs/ja-JP/skills/quarkus-tdd/SKILL.md b/docs/ja-JP/skills/quarkus-tdd/SKILL.md index 22ccf51a..643c84ea 100644 --- a/docs/ja-JP/skills/quarkus-tdd/SKILL.md +++ b/docs/ja-JP/skills/quarkus-tdd/SKILL.md @@ -34,19 +34,19 @@ origin: ECC @ExtendWith(MockitoExtension.class) @DisplayName("OrderService Unit Tests") class OrderServiceTest { - + @Mock private OrderRepository orderRepository; - + @Mock private EventService eventService; - + @Mock private FulfillmentPublisher fulfillmentPublisher; - + @InjectMocks private OrderService orderService; - + private CreateOrderCommand validCommand; @BeforeEach @@ -60,16 +60,16 @@ class OrderServiceTest { @Nested @DisplayName("createOrder のテスト") class CreateOrder { - + @Test @DisplayName("有効なコマンドが与えられた場合、注文を永続化してフルフィルメントイベントを発行する") void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() { // ARRANGE doNothing().when(orderRepository).persist(any(Order.class)); - + // ACT OrderReceipt receipt = orderService.createOrder(validCommand); - + // ASSERT assertThat(receipt).isNotNull(); assertThat(receipt.customerId()).isEqualTo("customer-123"); @@ -83,7 +83,7 @@ class OrderServiceTest { void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() { // ARRANGE CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines()); - + // ACT & ASSERT WebApplicationException exception = assertThrows( WebApplicationException.class, @@ -101,13 +101,13 @@ class OrderServiceTest { // ARRANGE doThrow(new PersistenceException("database unavailable")) .when(orderRepository).persist(any(Order.class)); - + // ACT & ASSERT PersistenceException exception = assertThrows( PersistenceException.class, () -> orderService.createOrder(validCommand) ); - + assertThat(exception.getMessage()).contains("database unavailable"); verify(eventService).createErrorEvent( eq(validCommand), @@ -125,7 +125,7 @@ class OrderServiceTest { NullPointerException.class, () -> orderService.createOrder(null) ); - + verify(orderRepository, never()).persist(any(Order.class)); } } @@ -184,7 +184,7 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); mockRabbitMQ.expectedMessageCount(1); - + // テスト用の実エンドポイントをモックに置き換え camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { @@ -192,13 +192,13 @@ class BusinessRulesRouteTest { advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT — .marshal().json(JsonLibrary.Jackson)の後、bodyはJSON文字列 mockRabbitMQ.assertIsSatisfied(5000); - + assertThat(mockRabbitMQ.getExchanges()).hasSize(1); String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); @@ -211,19 +211,19 @@ class BusinessRulesRouteTest { MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); camelContext.addEndpoint("mock:marshal", mockMarshal); mockMarshal.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.weaveAddLast().to("mock:marshal"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT mockMarshal.assertIsSatisfied(5000); - + String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); assertThat(body).contains("\"flowProfile\":\"BASIC\""); @@ -240,17 +240,17 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); mockInvoice.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); }); camelContext.getRouteController().startRoute("document-processing"); - + // ACT - producerTemplate.sendBodyAndHeader("direct:process-document", + producerTemplate.sendBodyAndHeader("direct:process-document", testPayload, "documentType", "INVOICE"); - + // ASSERT mockInvoice.assertIsSatisfied(5000); } @@ -261,23 +261,23 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); mockError.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:validation-error-handler.*") .replace().to("mock:error"); }); camelContext.getRouteController().startRoute("document-processing"); - + // バリデータビーンをモック化して例外をスロー when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document")); - + // ACT producerTemplate.sendBody("direct:process-document", testPayload); - + // ASSERT mockError.assertIsSatisfied(5000); - + Exception exception = mockError.getExchanges().get(0).getException(); assertThat(exception).isInstanceOf(ValidationException.class); assertThat(exception.getMessage()).contains("Invalid document"); @@ -295,13 +295,13 @@ class EventServiceTest { @Mock private EventRepository eventRepository; - + @Mock private ObjectMapper objectMapper; - + @InjectMocks private EventService eventService; - + private BusinessRulesPayload testPayload; @BeforeEach @@ -314,19 +314,19 @@ class EventServiceTest { @Nested @DisplayName("createSuccessEvent のテスト") class CreateSuccessEvent { - + @Test @DisplayName("有効なペイロードが与えられた場合、正しい属性でサクセスイベント作成") void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { // ARRANGE when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("DOCUMENT_PROCESSED") && event.getStatus() == EventStatus.SUCCESS && event.getPayload().equals("{\"documentId\":1}") && @@ -339,13 +339,13 @@ class EventServiceTest { void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { // ARRANGE Object nullPayload = null; - + // ACT & ASSERT NullPointerException exception = assertThrows( NullPointerException.class, () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") ); - + assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); verify(eventRepository, never()).persist(any()); } @@ -354,20 +354,20 @@ class EventServiceTest { @Nested @DisplayName("createErrorEvent のテスト") class CreateErrorEvent { - + @Test @DisplayName("エラーが与えられた場合、エラーメッセージ付きエラーイベント作成") void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { // ARRANGE String errorMessage = "Processing failed"; when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("PROCESSING_ERROR") && event.getStatus() == EventStatus.ERROR && event.getErrorMessage().equals(errorMessage) && @@ -384,7 +384,7 @@ class EventServiceTest { IllegalArgumentException.class, () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) ); - + assertThat(exception.getMessage()).contains("Error message cannot be blank"); } } @@ -400,13 +400,13 @@ class FileStorageServiceTest { @Mock private S3Client s3Client; - + @Mock private ExecutorService executorService; - + @InjectMocks private FileStorageService fileStorageService; - + private InputStream testInputStream; private LogContext testLogContext; @@ -421,7 +421,7 @@ class FileStorageServiceTest { @Nested @DisplayName("uploadOriginalFile のテスト") class UploadOriginalFile { - + @Test @DisplayName("有効なファイルが与えられた場合、ファイルアップロード成功とドキュメント情報を返す") void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { @@ -430,23 +430,23 @@ class FileStorageServiceTest { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenReturn(PutObjectResponse.builder().build()); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + StoredDocumentInfo result = future.join(); - + // ASSERT assertThat(result).isNotNull(); assertThat(result.getPath()).isNotBlank(); assertThat(result.getSize()).isEqualTo(1024L); assertThat(result.getUploadedAt()).isNotNull(); - + verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } @@ -458,15 +458,15 @@ class FileStorageServiceTest { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenThrow(new StorageException("S3 unavailable")); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + // ASSERT assertThatThrownBy(() -> future.join()) .isInstanceOf(CompletionException.class) @@ -479,17 +479,17 @@ class FileStorageServiceTest { void givenLogContext_whenUpload_thenContextPropagated() throws Exception { // ARRANGE AtomicReference capturedContext = new AtomicReference<>(); - + doAnswer(invocation -> { capturedContext.set(CustomLog.getCurrentContext()); ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + // ACT - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL).join(); - + // ASSERT assertThat(capturedContext.get()).isNotNull(); assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); @@ -641,7 +641,7 @@ class DocumentIntegrationTest { prepare-agent - + report @@ -650,7 +650,7 @@ class DocumentIntegrationTest { report - + check @@ -705,14 +705,14 @@ mvn jacoco:check quarkus-junit5-mockito test - + org.mockito mockito-core test - + org.assertj @@ -720,14 +720,14 @@ mvn jacoco:check 3.24.2 test - + io.rest-assured rest-assured test - + org.apache.camel.quarkus diff --git a/docs/ja-JP/skills/quarkus-verification/SKILL.md b/docs/ja-JP/skills/quarkus-verification/SKILL.md index df1e6ee8..0f11612a 100644 --- a/docs/ja-JP/skills/quarkus-verification/SKILL.md +++ b/docs/ja-JP/skills/quarkus-verification/SKILL.md @@ -437,28 +437,28 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Set up JDK 21 uses: actions/setup-java@v3 with: java-version: '21' distribution: 'temurin' - + - name: Cache Maven packages uses: actions/cache@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - + - name: Build run: mvn clean verify -DskipTests - + - name: Test with Coverage run: mvn test jacoco:report jacoco:check - + - name: Security Scan run: mvn org.owasp:dependency-check-maven:check - + - name: Upload Coverage uses: codecov/codecov-action@v3 with: diff --git a/docs/tr/skills/quarkus-patterns/SKILL.md b/docs/tr/skills/quarkus-patterns/SKILL.md index ee5d12d8..29a678a1 100644 --- a/docs/tr/skills/quarkus-patterns/SKILL.md +++ b/docs/tr/skills/quarkus-patterns/SKILL.md @@ -48,52 +48,52 @@ public class As2ProcessingService { public void processFile(Path filePath) throws Exception { LogContext logContext = CustomLog.getCurrentContext(); try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { - + String structureIdPartner = logContext.get(As2Constants.STRUCTURE_ID); - + // Koşullu akış mantığı boolean isChorusFlow = Boolean.parseBoolean(logContext.get(As2Constants.CHORUS_FLOW)); log.info("Is CHORUS_FLOW message: {}", isChorusFlow); - + ValidationFlowConfig validationFlowConfig = isChorusFlow ? ValidationFlowConfig.xsdOnly() : ValidationFlowConfig.allValidations(); - + InvoiceValidationResult invoiceValidationResult = this.invoiceFlowValidator - .validateFlowWithConfig(filePath, validationFlowConfig, + .validateFlowWithConfig(filePath, validationFlowConfig, EInvoiceSyntaxFormat.UBL, logContext); - + FlowProfile flowProfile = isChorusFlow ? FlowProfile.EXTENDED_CTC_FR : - this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult, + this.invoiceFlowValidator.computeFlowProfile(invoiceValidationResult, invoiceValidationResult.getInvoiceDetails().invoiceFormat().getProfile()); - + log.info("Invoice validation completed. Message is valid"); - + // CompletableFuture async işlemi try(InputStream inputStream = Files.newInputStream(filePath)) { - CompletableFuture documentInfoCompletableFuture = - fileStorageService.uploadOriginalFile(inputStream, - invoiceValidationResult.getSize(), logContext, + CompletableFuture documentInfoCompletableFuture = + fileStorageService.uploadOriginalFile(inputStream, + invoiceValidationResult.getSize(), logContext, invoiceValidationResult.getInvoiceFormat()); - + StoredDocumentInfo documentInfo = documentInfoCompletableFuture.join(); log.info("File uploaded successfully: {}", documentInfo.getPath()); - + if (StringUtils.isBlank(documentInfo.getPath())) { String errorMsg = "File path is empty after upload"; log.error(errorMsg); this.eventService.createErrorEvent(documentInfo, "FILE_UPLOAD_FAILED", errorMsg); throw new As2ServerProcessingException(errorMsg); } - + this.eventService.createSuccessEvent(documentInfo, "PERSISTENCE_BLOB_EVENT_TYPE"); - + String originalFileName = documentInfo.getOriginalFileName(); BusinessRulesPayload payload = this.documentJobService.createDocumentAndJobEntities( - documentInfo, originalFileName, structureIdPartner, + documentInfo, originalFileName, structureIdPartner, flowProfile, invoiceValidationResult.getDocumentHash()); - + // Async Camel yayınlama businessRulesPublisher.publishAsync(payload); this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT"); @@ -117,7 +117,7 @@ public class As2ProcessingService { ```java @ApplicationScoped public class ProcessingService { - + public void processDocument(Document doc) { LogContext logContext = CustomLog.getCurrentContext(); try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { @@ -125,12 +125,12 @@ public class ProcessingService { logContext.put("documentId", doc.getId().toString()); logContext.put("documentType", doc.getType()); logContext.put("userId", SecurityContext.getUserId()); - + log.info("Starting document processing"); - + // Bu kapsam içindeki tüm loglar bağlamı devralır processInternal(doc); - + log.info("Document processing completed"); } catch (Exception e) { log.error("Document processing failed", e); @@ -150,7 +150,7 @@ public class ProcessingService { true - + @@ -167,7 +167,7 @@ public class ProcessingService { public class EventService { private final EventRepository eventRepository; private final ObjectMapper objectMapper; - + public void createSuccessEvent(Object payload, String eventType) { Objects.requireNonNull(payload, "Payload cannot be null"); Event event = new Event(); @@ -175,11 +175,11 @@ public class EventService { event.setStatus(EventStatus.SUCCESS); event.setPayload(serializePayload(payload)); event.setTimestamp(Instant.now()); - + eventRepository.persist(event); log.info("Success event created: {}", eventType); } - + public void createErrorEvent(Object payload, String eventType, String errorMessage) { Objects.requireNonNull(payload, "Payload cannot be null"); if (errorMessage == null || errorMessage.isBlank()) { @@ -191,11 +191,11 @@ public class EventService { event.setErrorMessage(errorMessage); event.setPayload(serializePayload(payload)); event.setTimestamp(Instant.now()); - + eventRepository.persist(event); log.error("Error event created: {} - {}", eventType, errorMessage); } - + private String serializePayload(Object payload) { try { return objectMapper.writeValueAsString(payload); @@ -213,21 +213,21 @@ public class EventService { @RequiredArgsConstructor public class BusinessRulesPublisher { private final ProducerTemplate producerTemplate; - + @ConfigProperty(name = "camel.rabbitmq.queue.business-rules") String businessRulesQueue; - + public void publishAsync(BusinessRulesPayload payload) { producerTemplate.asyncSendBody( - "direct:business-rules-publisher", + "direct:business-rules-publisher", payload ); log.info("Message published to business rules queue: {}", payload.getDocumentId()); } - + public void publishSync(BusinessRulesPayload payload) { producerTemplate.sendBody( - "direct:business-rules-publisher", + "direct:business-rules-publisher", payload ); } @@ -239,23 +239,23 @@ public class BusinessRulesPublisher { ```java @ApplicationScoped public class BusinessRulesRoute extends RouteBuilder { - + @ConfigProperty(name = "camel.rabbitmq.queue.business-rules") String businessRulesQueue; - + @ConfigProperty(name = "rabbitmq.host") String rabbitHost; - + @ConfigProperty(name = "rabbitmq.port") Integer rabbitPort; - + @Override public void configure() { from("direct:business-rules-publisher") .routeId("business-rules-publisher") .log("Publishing message to RabbitMQ: ${body}") .marshal().json(JsonLibrary.Jackson) - .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", + .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", businessRulesQueue, rabbitHost, rabbitPort); } } @@ -266,7 +266,7 @@ public class BusinessRulesRoute extends RouteBuilder { ```java @ApplicationScoped public class DocumentProcessingRoute extends RouteBuilder { - + @Override public void configure() { // Hata yönetimi @@ -274,7 +274,7 @@ public class DocumentProcessingRoute extends RouteBuilder { .handled(true) .to("direct:validation-error-handler") .log("Validation error: ${exception.message}"); - + // Ana işleme route'u from("direct:process-document") .routeId("document-processing") @@ -289,7 +289,7 @@ public class DocumentProcessingRoute extends RouteBuilder { .otherwise() .to("direct:process-generic") .end(); - + from("direct:validation-error-handler") .bean(EventService.class, "createErrorEvent") .log("Validation error handled"); @@ -302,24 +302,24 @@ public class DocumentProcessingRoute extends RouteBuilder { ```java @ApplicationScoped public class FileMonitoringRoute extends RouteBuilder { - + @ConfigProperty(name = "file.input.directory") String inputDirectory; - + @ConfigProperty(name = "file.processed.directory") String processedDirectory; - + @ConfigProperty(name = "file.error.directory") String errorDirectory; - + @Override public void configure() { - from("file:" + inputDirectory + "?move=" + processedDirectory + + from("file:" + inputDirectory + "?move=" + processedDirectory + "&moveFailed=" + errorDirectory + "&delay=5000") .routeId("file-monitor") .log("Processing file: ${header.CamelFileName}") .to("direct:process-file"); - + from("direct:process-file") .bean(As2ProcessingService.class, "processFile") .log("File processing completed"); @@ -332,13 +332,13 @@ public class FileMonitoringRoute extends RouteBuilder { ```java @ApplicationScoped public class InvoiceRoute extends RouteBuilder { - + @Override public void configure() { from("direct:invoice-validation") .bean(InvoiceFlowValidator.class, "validateFlowWithConfig") .log("Validation result: ${body}"); - + from("direct:persist-and-publish") .bean(DocumentJobService.class, "createDocumentAndJobEntities") .bean(BusinessRulesPublisher.class, "publishAsync") @@ -391,7 +391,7 @@ public class DocumentResource { ```java @ApplicationScoped public class DocumentRepository implements PanacheRepository { - + public List findByStatus(DocumentStatus status, int page, int size) { return find("status = ?1 order by createdAt desc", status) .page(page, size) @@ -401,7 +401,7 @@ public class DocumentRepository implements PanacheRepository { public Optional findByReferenceNumber(String referenceNumber) { return find("referenceNumber", referenceNumber).firstResultOptional(); } - + public long countByStatusAndDate(DocumentStatus status, LocalDate date) { return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay()); } @@ -424,11 +424,11 @@ public class DocumentService { document.setDescription(request.description()); document.setStatus(DocumentStatus.PENDING); document.setCreatedAt(Instant.now()); - + repo.persist(document); - + eventService.createSuccessEvent(document, "DOCUMENT_CREATED"); - + return document; } @@ -455,7 +455,7 @@ public record CreateDocumentRequest( public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) { public static DocumentResponse from(Document document) { - return new DocumentResponse(document.getId(), document.getReferenceNumber(), + return new DocumentResponse(document.getId(), document.getReferenceNumber(), document.getStatus()); } } @@ -471,7 +471,7 @@ public class ValidationExceptionMapper implements ExceptionMapper cv.getPropertyPath() + ": " + cv.getMessage()) .collect(Collectors.joining(", ")); - + return Response.status(Response.Status.BAD_REQUEST) .entity(Map.of("error", "validation_error", "message", message)) .build(); @@ -501,29 +501,29 @@ public class GenericExceptionMapper implements ExceptionMapper { public class FileStorageService { private final S3Client s3Client; private final ExecutorService executorService; - + @ConfigProperty(name = "storage.bucket-name") String bucketName; - + public CompletableFuture uploadOriginalFile( - InputStream inputStream, - long size, + InputStream inputStream, + long size, LogContext logContext, InvoiceFormat format) { - + return CompletableFuture.supplyAsync(() -> { try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { String path = generateStoragePath(format); - + PutObjectRequest request = PutObjectRequest.builder() .bucket(bucketName) .key(path) .contentLength(size) .build(); - + s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size)); - + log.info("File uploaded to S3: {}", path); - + return new StoredDocumentInfo(path, size, Instant.now()); } catch (Exception e) { log.error("Failed to upload file to S3", e); @@ -569,7 +569,7 @@ public class DocumentCacheService { hibernate-orm: database: generation: drop-and-create - + rabbitmq: host: localhost port: 5672 @@ -595,7 +595,7 @@ public class DocumentCacheService { hibernate-orm: database: generation: validate - + rabbitmq: host: ${RABBITMQ_HOST} port: ${RABBITMQ_PORT} @@ -688,7 +688,7 @@ public class CamelHealthCheck implements HealthCheck { io.quarkus quarkus-config-yaml - + org.apache.camel.quarkus @@ -702,7 +702,7 @@ public class CamelHealthCheck implements HealthCheck { org.apache.camel.quarkus camel-quarkus-bean - + org.projectlombok @@ -710,7 +710,7 @@ public class CamelHealthCheck implements HealthCheck { ${lombok.version} provided - + io.quarkiverse.logging.logback diff --git a/docs/tr/skills/quarkus-security/SKILL.md b/docs/tr/skills/quarkus-security/SKILL.md index 8160c473..8ac75030 100644 --- a/docs/tr/skills/quarkus-security/SKILL.md +++ b/docs/tr/skills/quarkus-security/SKILL.md @@ -38,7 +38,7 @@ başlıklarını açıkça yapılandırın, gizli bilgileri Vault veya ortam de @Path("/api/protected") @Authenticated public class ProtectedResource { - + @Inject JsonWebToken jwt; @@ -75,20 +75,20 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET} @Provider @Priority(Priorities.AUTHENTICATION) public class CustomAuthFilter implements ContainerRequestFilter { - + @Inject SecurityIdentity identity; @Override public void filter(ContainerRequestContext requestContext) { String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - + // Başlık yoksa veya hatalıysa hemen reddet if (authHeader == null || !authHeader.startsWith("Bearer ")) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); return; } - + String token = authHeader.substring(7); if (!validateToken(token)) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); @@ -110,7 +110,7 @@ public class CustomAuthFilter implements ContainerRequestFilter { @Path("/api/admin") @RolesAllowed("ADMIN") public class AdminResource { - + @GET @Path("/users") public List listUsers() { @@ -128,7 +128,7 @@ public class AdminResource { @Path("/api/users") public class UserResource { - + @Inject SecurityIdentity securityIdentity; @@ -137,7 +137,7 @@ public class UserResource { @RolesAllowed("USER") public Response getUser(@PathParam("id") Long id) { // Sahipliği kontrol et - if (!securityIdentity.hasRole("ADMIN") && + if (!securityIdentity.hasRole("ADMIN") && !isOwner(id, securityIdentity.getPrincipal().getName())) { return Response.status(Response.Status.FORBIDDEN).build(); } @@ -155,7 +155,7 @@ public class UserResource { ```java @ApplicationScoped public class SecurityService { - + @Inject SecurityIdentity securityIdentity; @@ -163,7 +163,7 @@ public class SecurityService { if (securityIdentity.isAnonymous()) { return false; } - + if (securityIdentity.hasRole("ADMIN")) { return true; } @@ -239,7 +239,7 @@ List users = User.list("email = ?1 and active = ?2", email, true); Optional user = User.find("username", username).firstResultOptional(); // İYİ: İsimlendirilmiş parametreler -List users = User.list("email = :email and age > :minAge", +List users = User.list("email = :email and age > :minAge", Parameters.with("email", email).and("minAge", 18)); ``` @@ -266,7 +266,7 @@ public class User extends PanacheEntity { ```java @ApplicationScoped public class PasswordService { - + public String hash(String plainPassword) { return BcryptUtil.bcryptHash(plainPassword); } @@ -334,7 +334,7 @@ quarkus.vault.authentication.kubernetes.role=my-role ```java @ApplicationScoped public class SecretService { - + @ConfigProperty(name = "api-key") String apiKey; // Vault'tan alınır @@ -360,7 +360,7 @@ public class RateLimitFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) { String clientId = getClientIdentifier(); - RateLimiter limiter = limiters.computeIfAbsent(clientId, + RateLimiter limiter = limiters.computeIfAbsent(clientId, k -> RateLimiter.create(100.0)); // Saniyede 100 istek if (!limiter.tryAcquire()) { @@ -385,24 +385,24 @@ public class RateLimitFilter implements ContainerRequestFilter { ```java @Provider public class SecurityHeadersFilter implements ContainerResponseFilter { - + @Override public void filter(ContainerRequestContext request, ContainerResponseContext response) { MultivaluedMap headers = response.getHeaders(); - + // Clickjacking'i önle headers.putSingle("X-Frame-Options", "DENY"); - + // XSS koruması headers.putSingle("X-Content-Type-Options", "nosniff"); headers.putSingle("X-XSS-Protection", "1; mode=block"); - + // HSTS headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - + // CSP — script-src için 'unsafe-inline' kullanmayın, XSS korumasını etkisiz kılar; // bunun yerine nonce veya hash kullanın - headers.putSingle("Content-Security-Policy", + headers.putSingle("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"); } } @@ -419,11 +419,11 @@ public class AuditService { SecurityIdentity securityIdentity; public void logAccess(String resource, String action) { - String user = securityIdentity.isAnonymous() - ? "anonymous" + String user = securityIdentity.isAnonymous() + ? "anonymous" : securityIdentity.getPrincipal().getName(); - - LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", + + LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", user, action, resource, Instant.now()); } } diff --git a/docs/tr/skills/quarkus-tdd/SKILL.md b/docs/tr/skills/quarkus-tdd/SKILL.md index ab809312..c16a052b 100644 --- a/docs/tr/skills/quarkus-tdd/SKILL.md +++ b/docs/tr/skills/quarkus-tdd/SKILL.md @@ -36,25 +36,25 @@ Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izley @ExtendWith(MockitoExtension.class) @DisplayName("As2ProcessingService Unit Tests") class As2ProcessingServiceTest { - + @Mock private InvoiceFlowValidator invoiceFlowValidator; - + @Mock private EventService eventService; - + @Mock private DocumentJobService documentJobService; - + @Mock private BusinessRulesPublisher businessRulesPublisher; - + @Mock private FileStorageService fileStorageService; - + @InjectMocks private As2ProcessingService as2ProcessingService; - + private Path testFilePath; private LogContext testLogContext; private InvoiceValidationResult validationResult; @@ -64,17 +64,17 @@ class As2ProcessingServiceTest { void setUp() { // ARRANGE - Ortak test verisi testFilePath = Path.of("/tmp/test-invoice.xml"); - + testLogContext = new LogContext(); testLogContext.put(As2Constants.STRUCTURE_ID, "STRUCT-001"); testLogContext.put(As2Constants.FILE_NAME, "invoice.xml"); testLogContext.put(As2Constants.AS2_FROM, "PARTNER-001"); - + validationResult = new InvoiceValidationResult(); validationResult.setValid(true); validationResult.setSize(1024L); validationResult.setDocumentHash("abc123"); - + documentInfo = new StoredDocumentInfo(); documentInfo.setPath("s3://bucket/path/invoice.xml"); documentInfo.setSize(1024L); @@ -83,43 +83,43 @@ class As2ProcessingServiceTest { @Nested @DisplayName("processFile için testler") class ProcessFile { - + @Test @DisplayName("CHORUS olmayan dosyayı tüm validasyonlarla başarıyla işlemeli") void givenNonChorusFile_whenProcessFile_thenAllValidationsApplied() throws Exception { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "false"); CustomLog.setCurrentContext(testLogContext); - + when(invoiceFlowValidator.validateFlowWithConfig( - eq(testFilePath), + eq(testFilePath), eq(ValidationFlowConfig.allValidations()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class))) .thenReturn(validationResult); - + when(invoiceFlowValidator.computeFlowProfile(any(), any())) .thenReturn(FlowProfile.BASIC); - + when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(CompletableFuture.completedFuture(documentInfo)); - + when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), any(), any())) .thenReturn(new BusinessRulesPayload()); - + // ACT assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); - + // ASSERT verify(invoiceFlowValidator).validateFlowWithConfig( eq(testFilePath), eq(ValidationFlowConfig.allValidations()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class)); - - verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), + + verify(eventService).createSuccessEvent(any(StoredDocumentInfo.class), eq("PERSISTENCE_BLOB_EVENT_TYPE")); - verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), + verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class), eq("BUSINESS_RULES_MESSAGE_SENT")); verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class)); } @@ -130,34 +130,34 @@ class As2ProcessingServiceTest { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "true"); CustomLog.setCurrentContext(testLogContext); - + when(invoiceFlowValidator.validateFlowWithConfig( - eq(testFilePath), + eq(testFilePath), eq(ValidationFlowConfig.xsdOnly()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class))) .thenReturn(validationResult); - + when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(CompletableFuture.completedFuture(documentInfo)); - - when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), + + when(documentJobService.createDocumentAndJobEntities(any(), any(), any(), eq(FlowProfile.EXTENDED_CTC_FR), any())) .thenReturn(new BusinessRulesPayload()); - + // ACT assertDoesNotThrow(() -> as2ProcessingService.processFile(testFilePath)); - + // ASSERT verify(invoiceFlowValidator).validateFlowWithConfig( eq(testFilePath), eq(ValidationFlowConfig.xsdOnly()), eq(EInvoiceSyntaxFormat.UBL), any(LogContext.class)); - + verify(documentJobService).createDocumentAndJobEntities( - any(), any(), any(), - eq(FlowProfile.EXTENDED_CTC_FR), + any(), any(), any(), + eq(FlowProfile.EXTENDED_CTC_FR), any()); } @@ -167,31 +167,31 @@ class As2ProcessingServiceTest { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "false"); CustomLog.setCurrentContext(testLogContext); - + when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) .thenReturn(validationResult); - + when(invoiceFlowValidator.computeFlowProfile(any(), any())) .thenReturn(FlowProfile.BASIC); - + documentInfo.setPath(""); // Boş path hatayı tetikler when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(CompletableFuture.completedFuture(documentInfo)); - + // ACT & ASSERT As2ServerProcessingException exception = assertThrows( As2ServerProcessingException.class, () -> as2ProcessingService.processFile(testFilePath) ); - + assertThat(exception.getMessage()) .contains("File path is empty after upload"); - + verify(eventService).createErrorEvent( - eq(documentInfo), - eq("FILE_UPLOAD_FAILED"), + eq(documentInfo), + eq("FILE_UPLOAD_FAILED"), contains("File path is empty")); - + verify(businessRulesPublisher, never()).publishAsync(any()); } @@ -201,18 +201,18 @@ class As2ProcessingServiceTest { // ARRANGE testLogContext.put(As2Constants.CHORUS_FLOW, "false"); CustomLog.setCurrentContext(testLogContext); - + when(invoiceFlowValidator.validateFlowWithConfig(any(), any(), any(), any())) .thenReturn(validationResult); - + when(invoiceFlowValidator.computeFlowProfile(any(), any())) .thenReturn(FlowProfile.BASIC); - - CompletableFuture failedFuture = + + CompletableFuture failedFuture = CompletableFuture.failedFuture(new StorageException("S3 connection failed")); when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any())) .thenReturn(failedFuture); - + // ACT & ASSERT assertThrows( CompletionException.class, @@ -225,13 +225,13 @@ class As2ProcessingServiceTest { void givenNullFilePath_whenProcessFile_thenThrowsException() { // ARRANGE Path nullPath = null; - + // ACT & ASSERT NullPointerException exception = assertThrows( NullPointerException.class, () -> as2ProcessingService.processFile(nullPath) ); - + verify(invoiceFlowValidator, never()).validateFlowWithConfig(any(), any(), any(), any()); } } @@ -287,7 +287,7 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); mockRabbitMQ.expectedMessageCount(1); - + // Test için gerçek endpoint'i mock ile değiştir camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { @@ -295,13 +295,13 @@ class BusinessRulesRouteTest { advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT — .marshal().json() sonrası body JSON String'dir mockRabbitMQ.assertIsSatisfied(5000); - + assertThat(mockRabbitMQ.getExchanges()).hasSize(1); String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); @@ -314,19 +314,19 @@ class BusinessRulesRouteTest { MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); camelContext.addEndpoint("mock:marshal", mockMarshal); mockMarshal.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.weaveAddLast().to("mock:marshal"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT mockMarshal.assertIsSatisfied(5000); - + String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); assertThat(body).contains("\"flowProfile\":\"BASIC\""); @@ -343,17 +343,17 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); mockInvoice.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); }); camelContext.getRouteController().startRoute("document-processing"); - + // ACT - producerTemplate.sendBodyAndHeader("direct:process-document", + producerTemplate.sendBodyAndHeader("direct:process-document", testPayload, "documentType", "INVOICE"); - + // ASSERT mockInvoice.assertIsSatisfied(5000); } @@ -364,25 +364,25 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); mockError.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:validation-error-handler.*") .replace().to("mock:error"); }); camelContext.getRouteController().startRoute("document-processing"); - + // Error event oluşturma hatasını gerçek EventService API'si üzerinden simüle et doThrow(new ValidationException("Invalid document")) .when(eventService) .createErrorEvent(any(), eq("VALIDATION_ERROR"), anyString()); - + // ACT producerTemplate.sendBody("direct:process-document", testPayload); - + // ASSERT mockError.assertIsSatisfied(5000); - + Exception exception = mockError.getExchanges().get(0).getException(); assertThat(exception).isInstanceOf(ValidationException.class); assertThat(exception.getMessage()).contains("Invalid document"); @@ -400,13 +400,13 @@ class EventServiceTest { @Mock private EventRepository eventRepository; - + @Mock private ObjectMapper objectMapper; - + @InjectMocks private EventService eventService; - + private BusinessRulesPayload testPayload; @BeforeEach @@ -419,19 +419,19 @@ class EventServiceTest { @Nested @DisplayName("createSuccessEvent için testler") class CreateSuccessEvent { - + @Test @DisplayName("Doğru niteliklerle başarı eventi oluşturulmalı") void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { // ARRANGE when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("DOCUMENT_PROCESSED") && event.getStatus() == EventStatus.SUCCESS && event.getPayload().equals("{\"documentId\":1}") && @@ -444,13 +444,13 @@ class EventServiceTest { void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { // ARRANGE Object nullPayload = null; - + // ACT & ASSERT NullPointerException exception = assertThrows( NullPointerException.class, () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") ); - + assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); verify(eventRepository, never()).persist(any()); } @@ -459,20 +459,20 @@ class EventServiceTest { @Nested @DisplayName("createErrorEvent için testler") class CreateErrorEvent { - + @Test @DisplayName("Hata mesajıyla hata eventi oluşturulmalı") void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { // ARRANGE String errorMessage = "Processing failed"; when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("PROCESSING_ERROR") && event.getStatus() == EventStatus.ERROR && event.getErrorMessage().equals(errorMessage) && @@ -489,7 +489,7 @@ class EventServiceTest { IllegalArgumentException.class, () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) ); - + assertThat(exception.getMessage()).contains("Error message cannot be blank"); } } @@ -505,13 +505,13 @@ class FileStorageServiceTest { @Mock private S3Client s3Client; - + @Mock private ExecutorService executorService; - + @InjectMocks private FileStorageService fileStorageService; - + private InputStream testInputStream; private LogContext testLogContext; @@ -526,7 +526,7 @@ class FileStorageServiceTest { @Nested @DisplayName("uploadOriginalFile için testler") class UploadOriginalFile { - + @Test @DisplayName("Dosyayı başarıyla yüklemeli ve belge bilgisi döndürmeli") void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { @@ -535,23 +535,23 @@ class FileStorageServiceTest { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenReturn(PutObjectResponse.builder().build()); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + StoredDocumentInfo result = future.join(); - + // ASSERT assertThat(result).isNotNull(); assertThat(result.getPath()).isNotBlank(); assertThat(result.getSize()).isEqualTo(1024L); assertThat(result.getUploadedAt()).isNotNull(); - + verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } @@ -566,12 +566,12 @@ class FileStorageServiceTest { when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenThrow(new StorageException("S3 unavailable")); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + // ASSERT assertThatThrownBy(() -> future.join()) .isInstanceOf(CompletionException.class) @@ -584,17 +584,17 @@ class FileStorageServiceTest { void givenLogContext_whenUpload_thenContextPropagated() throws Exception { // ARRANGE AtomicReference capturedContext = new AtomicReference<>(); - + doAnswer(invocation -> { capturedContext.set(CustomLog.getCurrentContext()); ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + // ACT - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL).join(); - + // ASSERT assertThat(capturedContext.get()).isNotNull(); assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); @@ -746,7 +746,7 @@ class DocumentIntegrationTest { prepare-agent - + report @@ -755,7 +755,7 @@ class DocumentIntegrationTest { report - + check @@ -810,14 +810,14 @@ mvn jacoco:check quarkus-junit5-mockito test - + org.mockito mockito-core test - + org.assertj @@ -825,14 +825,14 @@ mvn jacoco:check 3.24.2 test - + io.rest-assured rest-assured test - + org.apache.camel.quarkus diff --git a/docs/tr/skills/quarkus-verification/SKILL.md b/docs/tr/skills/quarkus-verification/SKILL.md index 8d687949..b7d42366 100644 --- a/docs/tr/skills/quarkus-verification/SKILL.md +++ b/docs/tr/skills/quarkus-verification/SKILL.md @@ -437,28 +437,28 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Set up JDK 21 uses: actions/setup-java@v3 with: java-version: '21' distribution: 'temurin' - + - name: Cache Maven packages uses: actions/cache@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - + - name: Build run: mvn clean verify -DskipTests - + - name: Test with Coverage run: mvn test jacoco:report jacoco:check - + - name: Security Scan run: mvn org.owasp:dependency-check-maven:check - + - name: Upload Coverage uses: codecov/codecov-action@v3 with: diff --git a/docs/ur/README.md b/docs/ur/README.md index f3b4d7d0..64e78003 100644 --- a/docs/ur/README.md +++ b/docs/ur/README.md @@ -47,26 +47,26 @@ ECC v2.0.0-rc.1 اس قابل استعمال پرت پر عوامی Hermes آپ - 💼 ECC Pro
+ ECC Pro
نجی ریپوز · GitHub App · $19/نشست/ماہ
- ❤️ اسپانسر
+ اسپانسر
OSS کو فنڈ کریں · $5/ماہ سے
- 💬 کمیونٹی + کمیونٹی
Discussions · Q&A · Show & Tell
- 🤖 GitHub App
+ GitHub App
انسٹال · PR آڈٹس · مفت ٹیئر
diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index f37808d7..241f675c 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 63 个专业代理、251 项技能、79 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 64 个专业代理、255 项技能、79 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.0.0-rc.1 @@ -146,8 +146,8 @@ ## 项目结构 ``` -agents/ — 63 个专业子代理 -skills/ — 251 个工作流技能和领域知识 +agents/ — 64 个专业子代理 +skills/ — 255 个工作流技能和领域知识 commands/ — 79 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 0014cf38..9cb11465 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 63 个智能体、251 项技能和 79 个命令了。 +**搞定!** 你现在可以使用 64 个智能体、255 项技能和 79 个命令了。 *** @@ -1136,9 +1136,9 @@ opencode | 功能特性 | Claude Code | OpenCode | 状态 | |---------|---------------|----------|--------| -| 智能体 | PASS: 63 个 | PASS: 12 个 | **Claude Code 领先** | +| 智能体 | PASS: 64 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 79 个 | PASS: 35 个 | **Claude Code 领先** | -| 技能 | PASS: 251 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 255 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1244,9 +1244,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 | 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|-----------------------|------------|-----------|----------| -| **智能体** | 63 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | +| **智能体** | 64 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 79 | 共享 | 基于指令 | 35 | -| **技能** | 251 | 共享 | 10 (原生格式) | 37 | +| **技能** | 255 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/scripts/hooks/plugin-hook-bootstrap.js b/scripts/hooks/plugin-hook-bootstrap.js index c965d403..cc6724ee 100644 --- a/scripts/hooks/plugin-hook-bootstrap.js +++ b/scripts/hooks/plugin-hook-bootstrap.js @@ -171,7 +171,14 @@ function main() { process.exit(Number.isInteger(result.status) ? result.status : 0); } -if (require.main === module) { +// Run when invoked as a hook entry. Production hooks load this via +// `node -e "...; process.argv.splice(1,0,s); require(s)"`; on Node 21+ that +// leaves require.main undefined (not this module), which previously skipped +// main() and made every plugin hook a silent no-op. Guard on both the +// direct-entry case and that eval-bootstrap case. When imported for its +// exports (tests), require.main is a real, different module, so main() stays +// dormant. +if (require.main === module || require.main === undefined) { main(); } diff --git a/skills/codehealth-mcp/SKILL.md b/skills/codehealth-mcp/SKILL.md index 1f41e585..08d8ee47 100644 --- a/skills/codehealth-mcp/SKILL.md +++ b/skills/codehealth-mcp/SKILL.md @@ -8,7 +8,7 @@ origin: community Structural maintainability feedback for AI-assisted coding. Complements style/lint skills (`coding-standards`, `plankton-code-quality`) with **design-level** health scores and regression gates. -**Upstream:** [codescene-oss/codescene-mcp-server](https://github.com/codescene-oss/codescene-mcp-server) +**Upstream:** [codescene-oss/codescene-mcp-server](https://github.com/codescene-oss/codescene-mcp-server) **Package:** `@codescene/codehealth-mcp` (stdio via npx) ## Security and boundaries diff --git a/skills/fastapi-patterns/SKILL.md b/skills/fastapi-patterns/SKILL.md index 3e7ae88f..34ae89a7 100644 --- a/skills/fastapi-patterns/SKILL.md +++ b/skills/fastapi-patterns/SKILL.md @@ -510,4 +510,4 @@ async def list_items(db: AsyncSession = Depends(get_db)): - Wrap database mutation boundaries gracefully within transactions inside your service layer, catching structural database errors directly. - Parse JWT parameters defensively, expecting potential string/integer cast mismatches from modern payload variations. - Enforce deterministic sorting (e.g., `.order_by(Model.id)`) on all offset/limit paginated endpoints to avoid data skips. -- Isolate authorization checks from core authentication dependencies to provide precise REST status signals (`401` vs `403`). \ No newline at end of file +- Isolate authorization checks from core authentication dependencies to provide precise REST status signals (`401` vs `403`). diff --git a/skills/frontend-a11y/SKILL.md b/skills/frontend-a11y/SKILL.md index 08763c1c..efdf492a 100644 --- a/skills/frontend-a11y/SKILL.md +++ b/skills/frontend-a11y/SKILL.md @@ -203,8 +203,7 @@ Use ARIA only when native HTML semantics are insufficient. Wrong ARIA is worse t

This action cannot be undone.

``` diff --git a/skills/homelab-pihole-dns/SKILL.md b/skills/homelab-pihole-dns/SKILL.md index 0048749c..621cf59e 100644 --- a/skills/homelab-pihole-dns/SKILL.md +++ b/skills/homelab-pihole-dns/SKILL.md @@ -96,11 +96,11 @@ less pi-hole-install.sh # review before proceeding bash pi-hole-install.sh # Follow the interactive installer: -# 1. Select network interface (eth0 for wired — recommended) -# 2. Select upstream DNS (Cloudflare or leave default — can change later) -# 3. Confirm static IP -# 4. Install the web admin interface (recommended) -# 5. Note the admin password shown at the end +# 1. Select network interface (eth0 for wired — recommended) +# 2. Select upstream DNS (Cloudflare or leave default — can change later) +# 3. Confirm static IP +# 4. Install the web admin interface (recommended) +# 5. Note the admin password shown at the end ``` ## Pointing Your Network at Pi-hole @@ -183,9 +183,9 @@ sudo systemctl start cloudflared sudo systemctl enable cloudflared # Now point Pi-hole at the local DoH proxy: -# Pi-hole admin → Settings → DNS → Custom upstream DNS -# Set to: 127.0.0.1#5053 -# Uncheck all other upstream resolvers +# Pi-hole admin → Settings → DNS → Custom upstream DNS +# Set to: 127.0.0.1#5053 +# Uncheck all other upstream resolvers ``` ## Local DNS Records diff --git a/skills/homelab-wireguard-vpn/SKILL.md b/skills/homelab-wireguard-vpn/SKILL.md index 65b58fb7..39a53ff6 100644 --- a/skills/homelab-wireguard-vpn/SKILL.md +++ b/skills/homelab-wireguard-vpn/SKILL.md @@ -218,8 +218,8 @@ stays reachable after an IP change. restart: unless-stopped # ddns.env (chmod 600, not committed to git): -# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id -# SETTINGS_CLOUDFLARE_TOKEN=your_api_token +# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id +# SETTINGS_CLOUDFLARE_TOKEN=your_api_token # Option 2: DuckDNS (free, simple) Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org) diff --git a/skills/inherit-legacy-style/SKILL.md b/skills/inherit-legacy-style/SKILL.md index bc3973c5..dda2e0ce 100644 --- a/skills/inherit-legacy-style/SKILL.md +++ b/skills/inherit-legacy-style/SKILL.md @@ -71,9 +71,9 @@ Before interrupting the user, evaluate signal strength: For each strong-signal conflict, present exactly ONE question with 4 options: -> 📄 Evidence: `pathA` uses style X, `pathB` uses style Y -> ⚠️ Risk: mixing both fractures the project style -> ❓ Choose: `1` follow X `2` follow Y `3` this is evolution, update rules `4` I have a new rule +> Evidence: `pathA` uses style X, `pathB` uses style Y +> WARNING: Risk: mixing both fractures the project style +> Choose: `1` follow X `2` follow Y `3` this is evolution, update rules `4` I have a new rule Suspend until the user answers, then proceed to the next conflict. Never stack questions. @@ -89,7 +89,7 @@ Ask the user for enforcement strength (use `AskUserQuestion`): | Option | Mechanism | |---|---| | **1** Soft hook (recommended) | Write `@.ai-style-rules.md` reference into project `CLAUDE.md` | -| **2** Hard hook | Soft hook + `PreToolUse[Write|Edit|MultiEdit]` Hook in `settings.json` | +| **2** Hard hook | Soft hook + `PreToolUse[Write\|Edit\|MultiEdit]` Hook in `settings.json` | | **3** No hook | Keep the rules file; user references manually | ### Branch B — Incremental Sniff @@ -120,12 +120,12 @@ This skill auto-detects whether it's a first-time or incremental run via `.ai-st ## Anti-Patterns -- ❌ Do NOT skip the scale measurement step — sampling a 30-file project "starves" it; full-scanning a 5,000-file repo blows up -- ❌ Do NOT stack multiple conflict questions at once — grilling is strictly one-at-a-time -- ❌ Do NOT overwrite old rules in incremental mode — always append evolution logs -- ❌ Do NOT default to "hard hook" without asking — enforcement strength is the user's call -- ❌ Do NOT judge syntax or tech-stack quality — this skill aligns meta-architecture only -- ❌ Do NOT copy bugs from exemplar files — reuse structure, flag defects +- FAIL: Do NOT skip the scale measurement step — sampling a 30-file project "starves" it; full-scanning a 5,000-file repo blows up +- FAIL: Do NOT stack multiple conflict questions at once — grilling is strictly one-at-a-time +- FAIL: Do NOT overwrite old rules in incremental mode — always append evolution logs +- FAIL: Do NOT default to "hard hook" without asking — enforcement strength is the user's call +- FAIL: Do NOT judge syntax or tech-stack quality — this skill aligns meta-architecture only +- FAIL: Do NOT copy bugs from exemplar files — reuse structure, flag defects ## Best Practices diff --git a/skills/motion-patterns/SKILL.md b/skills/motion-patterns/SKILL.md index 16425d18..a883ea45 100644 --- a/skills/motion-patterns/SKILL.md +++ b/skills/motion-patterns/SKILL.md @@ -337,8 +337,7 @@ export function ExpandingCard({ title, body }: { title: string; body: string }) duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth, }} -> - {children} +> {children} ``` diff --git a/skills/quarkus-patterns/SKILL.md b/skills/quarkus-patterns/SKILL.md index 3de2cc71..c1526c48 100644 --- a/skills/quarkus-patterns/SKILL.md +++ b/skills/quarkus-patterns/SKILL.md @@ -70,7 +70,7 @@ public class OrderProcessingService { ```java @ApplicationScoped public class ProcessingService { - + public void processDocument(Document doc) { LogContext logContext = CustomLog.getCurrentContext(); try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { @@ -78,12 +78,12 @@ public class ProcessingService { logContext.put("documentId", doc.getId().toString()); logContext.put("documentType", doc.getType()); logContext.put("userId", SecurityContext.getUserId()); - + log.info("Starting document processing"); - + // All logs within this scope inherit the context processInternal(doc); - + log.info("Document processing completed"); } catch (Exception e) { log.error("Document processing failed", e); @@ -103,7 +103,7 @@ public class ProcessingService { true - + @@ -120,7 +120,7 @@ public class ProcessingService { public class EventService { private final EventRepository eventRepository; private final ObjectMapper objectMapper; - + public void createSuccessEvent(Object payload, String eventType) { Objects.requireNonNull(payload, "Payload cannot be null"); Event event = new Event(); @@ -128,11 +128,11 @@ public class EventService { event.setStatus(EventStatus.SUCCESS); event.setPayload(serializePayload(payload)); event.setTimestamp(Instant.now()); - + eventRepository.persist(event); log.info("Success event created: {}", eventType); } - + public void createErrorEvent(Object payload, String eventType, String errorMessage) { Objects.requireNonNull(payload, "Payload cannot be null"); if (errorMessage == null || errorMessage.isBlank()) { @@ -144,11 +144,11 @@ public class EventService { event.setErrorMessage(errorMessage); event.setPayload(serializePayload(payload)); event.setTimestamp(Instant.now()); - + eventRepository.persist(event); log.error("Error event created: {} - {}", eventType, errorMessage); } - + private String serializePayload(Object payload) { try { return objectMapper.writeValueAsString(payload); @@ -167,10 +167,10 @@ public class EventService { @RequiredArgsConstructor public class BusinessRulesPublisher { private final ProducerTemplate producerTemplate; - + public void publishSync(BusinessRulesPayload payload) { producerTemplate.sendBody( - "direct:business-rules-publisher", + "direct:business-rules-publisher", payload ); } @@ -182,23 +182,23 @@ public class BusinessRulesPublisher { ```java @ApplicationScoped public class BusinessRulesRoute extends RouteBuilder { - + @ConfigProperty(name = "camel.rabbitmq.queue.business-rules") String businessRulesQueue; - + @ConfigProperty(name = "rabbitmq.host") String rabbitHost; - + @ConfigProperty(name = "rabbitmq.port") Integer rabbitPort; - + @Override public void configure() { from("direct:business-rules-publisher") .routeId("business-rules-publisher") .log("Publishing message to RabbitMQ: ${body}") .marshal().json(JsonLibrary.Jackson) - .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", + .toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d", businessRulesQueue, rabbitHost, rabbitPort); } } @@ -209,7 +209,7 @@ public class BusinessRulesRoute extends RouteBuilder { ```java @ApplicationScoped public class DocumentProcessingRoute extends RouteBuilder { - + @Override public void configure() { // Error handling @@ -217,7 +217,7 @@ public class DocumentProcessingRoute extends RouteBuilder { .handled(true) .to("direct:validation-error-handler") .log("Validation error: ${exception.message}"); - + // Main processing route from("direct:process-document") .routeId("document-processing") @@ -232,7 +232,7 @@ public class DocumentProcessingRoute extends RouteBuilder { .otherwise() .to("direct:process-generic") .end(); - + from("direct:validation-error-handler") .bean(EventService.class, "createErrorEvent") .log("Validation error handled"); @@ -245,24 +245,24 @@ public class DocumentProcessingRoute extends RouteBuilder { ```java @ApplicationScoped public class FileMonitoringRoute extends RouteBuilder { - + @ConfigProperty(name = "file.input.directory") String inputDirectory; - + @ConfigProperty(name = "file.processed.directory") String processedDirectory; - + @ConfigProperty(name = "file.error.directory") String errorDirectory; - + @Override public void configure() { - from("file:" + inputDirectory + "?move=" + processedDirectory + + from("file:" + inputDirectory + "?move=" + processedDirectory + "&moveFailed=" + errorDirectory + "&delay=5000") .routeId("file-monitor") .log("Processing file: ${header.CamelFileName}") .to("direct:process-file"); - + from("direct:process-file") .bean(OrderProcessingService.class, "processFile") .log("File processing completed"); @@ -275,13 +275,13 @@ public class FileMonitoringRoute extends RouteBuilder { ```java @ApplicationScoped public class InvoiceRoute extends RouteBuilder { - + @Override public void configure() { from("direct:invoice-validation") .bean(InvoiceFlowValidator.class, "validateFlowWithConfig") .log("Validation result: ${body}"); - + from("direct:persist-and-publish") .bean(DocumentJobService.class, "createDocumentAndJobEntities") .bean(BusinessRulesPublisher.class, "publishAsync") @@ -334,7 +334,7 @@ public class DocumentResource { ```java @ApplicationScoped public class DocumentRepository implements PanacheRepository { - + public List findByStatus(DocumentStatus status, int page, int size) { return find("status = ?1 order by createdAt desc", status) .page(page, size) @@ -344,7 +344,7 @@ public class DocumentRepository implements PanacheRepository { public Optional findByReferenceNumber(String referenceNumber) { return find("referenceNumber", referenceNumber).firstResultOptional(); } - + public long countByStatusAndDate(DocumentStatus status, LocalDate date) { return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay()); } @@ -367,11 +367,11 @@ public class DocumentService { document.setDescription(request.description()); document.setStatus(DocumentStatus.PENDING); document.setCreatedAt(Instant.now()); - + repo.persist(document); - + eventService.createSuccessEvent(document, "DOCUMENT_CREATED"); - + return document; } @@ -398,7 +398,7 @@ public record CreateDocumentRequest( public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) { public static DocumentResponse from(Document document) { - return new DocumentResponse(document.getId(), document.getReferenceNumber(), + return new DocumentResponse(document.getId(), document.getReferenceNumber(), document.getStatus()); } } @@ -414,7 +414,7 @@ public class ValidationExceptionMapper implements ExceptionMapper cv.getPropertyPath() + ": " + cv.getMessage()) .collect(Collectors.joining(", ")); - + return Response.status(Response.Status.BAD_REQUEST) .entity(Map.of("error", "validation_error", "message", message)) .build(); @@ -444,30 +444,30 @@ public class GenericExceptionMapper implements ExceptionMapper { public class FileStorageService { private final S3Client s3Client; private final ExecutorService executorService; - + @ConfigProperty(name = "storage.bucket-name") String bucketName; - + public CompletableFuture uploadOriginalFile( - InputStream inputStream, - long size, + InputStream inputStream, + long size, LogContext logContext, InvoiceFormat format) { - + return CompletableFuture.supplyAsync(() -> { try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) { String path = generateStoragePath(format); - + PutObjectRequest request = PutObjectRequest.builder() .bucket(bucketName) .key(path) .contentLength(size) .build(); - + s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size)); - + log.info("File uploaded to S3: {}", path); - + return new StoredDocumentInfo(path, size, Instant.now()); } catch (Exception e) { log.error("Failed to upload file to S3", e); @@ -513,7 +513,7 @@ public class DocumentCacheService { hibernate-orm: database: generation: drop-and-create - + rabbitmq: host: localhost port: 5672 @@ -539,7 +539,7 @@ public class DocumentCacheService { hibernate-orm: database: generation: validate - + rabbitmq: host: ${RABBITMQ_HOST} port: ${RABBITMQ_PORT} @@ -632,7 +632,7 @@ public class CamelHealthCheck implements HealthCheck { io.quarkus quarkus-config-yaml
- + org.apache.camel.quarkus @@ -646,7 +646,7 @@ public class CamelHealthCheck implements HealthCheck { org.apache.camel.quarkus camel-quarkus-bean - + org.projectlombok @@ -654,7 +654,7 @@ public class CamelHealthCheck implements HealthCheck { ${lombok.version} provided - + io.quarkiverse.logging.logback diff --git a/skills/quarkus-security/SKILL.md b/skills/quarkus-security/SKILL.md index 4a9af479..0222646b 100644 --- a/skills/quarkus-security/SKILL.md +++ b/skills/quarkus-security/SKILL.md @@ -28,7 +28,7 @@ Best practices for securing Quarkus applications with authentication, authorizat @Path("/api/protected") @Authenticated public class ProtectedResource { - + @Inject JsonWebToken jwt; @@ -65,20 +65,20 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET} @Provider @Priority(Priorities.AUTHENTICATION) public class CustomAuthFilter implements ContainerRequestFilter { - + @Inject SecurityIdentity identity; @Override public void filter(ContainerRequestContext requestContext) { String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - + // Reject immediately if header is absent or malformed if (authHeader == null || !authHeader.startsWith("Bearer ")) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); return; } - + String token = authHeader.substring(7); if (!validateToken(token)) { requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); @@ -100,7 +100,7 @@ public class CustomAuthFilter implements ContainerRequestFilter { @Path("/api/admin") @RolesAllowed("ADMIN") public class AdminResource { - + @GET @Path("/users") public List listUsers() { @@ -118,7 +118,7 @@ public class AdminResource { @Path("/api/users") public class UserResource { - + @Inject SecurityIdentity securityIdentity; @@ -127,7 +127,7 @@ public class UserResource { @RolesAllowed("USER") public Response getUser(@PathParam("id") Long id) { // Check ownership - if (!securityIdentity.hasRole("ADMIN") && + if (!securityIdentity.hasRole("ADMIN") && !isOwner(id, securityIdentity.getPrincipal().getName())) { return Response.status(Response.Status.FORBIDDEN).build(); } @@ -145,7 +145,7 @@ public class UserResource { ```java @ApplicationScoped public class SecurityService { - + @Inject SecurityIdentity securityIdentity; @@ -153,7 +153,7 @@ public class SecurityService { if (securityIdentity.isAnonymous()) { return false; } - + if (securityIdentity.hasRole("ADMIN")) { return true; } @@ -229,7 +229,7 @@ List users = User.list("email = ?1 and active = ?2", email, true); Optional user = User.find("username", username).firstResultOptional(); // GOOD: Named parameters -List users = User.list("email = :email and age > :minAge", +List users = User.list("email = :email and age > :minAge", Parameters.with("email", email).and("minAge", 18)); ``` @@ -256,7 +256,7 @@ public class User extends PanacheEntity { ```java @ApplicationScoped public class PasswordService { - + public String hash(String plainPassword) { return BcryptUtil.bcryptHash(plainPassword); } @@ -324,7 +324,7 @@ quarkus.vault.authentication.kubernetes.role=my-role ```java @ApplicationScoped public class SecretService { - + @ConfigProperty(name = "api-key") String apiKey; // Fetched from Vault @@ -351,7 +351,7 @@ public class RateLimitFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) { String clientId = getClientIdentifier(); - RateLimiter limiter = limiters.computeIfAbsent(clientId, + RateLimiter limiter = limiters.computeIfAbsent(clientId, k -> RateLimiter.create(100.0)); // 100 requests per second if (!limiter.tryAcquire()) { @@ -377,25 +377,25 @@ public class RateLimitFilter implements ContainerRequestFilter { ```java @Provider public class SecurityHeadersFilter implements ContainerResponseFilter { - + @Override public void filter(ContainerRequestContext request, ContainerResponseContext response) { MultivaluedMap headers = response.getHeaders(); - + // Prevent clickjacking headers.putSingle("X-Frame-Options", "DENY"); - + // XSS protection headers.putSingle("X-Content-Type-Options", "nosniff"); headers.putSingle("X-XSS-Protection", "1; mode=block"); - + // HSTS headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); - + // CSP — avoid 'unsafe-inline' for script-src as it negates XSS protection; // use nonces or hashes instead. 'unsafe-inline' for style-src is acceptable // when CSS frameworks require it, but prefer nonces where possible. - headers.putSingle("Content-Security-Policy", + headers.putSingle("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"); } } @@ -412,11 +412,11 @@ public class AuditService { SecurityIdentity securityIdentity; public void logAccess(String resource, String action) { - String user = securityIdentity.isAnonymous() - ? "anonymous" + String user = securityIdentity.isAnonymous() + ? "anonymous" : securityIdentity.getPrincipal().getName(); - - LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", + + LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s", user, action, resource, Instant.now()); } } diff --git a/skills/quarkus-tdd/SKILL.md b/skills/quarkus-tdd/SKILL.md index beae1952..7529b590 100644 --- a/skills/quarkus-tdd/SKILL.md +++ b/skills/quarkus-tdd/SKILL.md @@ -34,19 +34,19 @@ Follow this structured approach for comprehensive, readable tests: @ExtendWith(MockitoExtension.class) @DisplayName("OrderService Unit Tests") class OrderServiceTest { - + @Mock private OrderRepository orderRepository; - + @Mock private EventService eventService; - + @Mock private FulfillmentPublisher fulfillmentPublisher; - + @InjectMocks private OrderService orderService; - + private CreateOrderCommand validCommand; @BeforeEach @@ -60,16 +60,16 @@ class OrderServiceTest { @Nested @DisplayName("Tests for createOrder") class CreateOrder { - + @Test @DisplayName("Should persist order and publish fulfillment event") void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() { // ARRANGE doNothing().when(orderRepository).persist(any(Order.class)); - + // ACT OrderReceipt receipt = orderService.createOrder(validCommand); - + // ASSERT assertThat(receipt).isNotNull(); assertThat(receipt.customerId()).isEqualTo("customer-123"); @@ -83,7 +83,7 @@ class OrderServiceTest { void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() { // ARRANGE CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines()); - + // ACT & ASSERT WebApplicationException exception = assertThrows( WebApplicationException.class, @@ -101,13 +101,13 @@ class OrderServiceTest { // ARRANGE doThrow(new PersistenceException("database unavailable")) .when(orderRepository).persist(any(Order.class)); - + // ACT & ASSERT PersistenceException exception = assertThrows( PersistenceException.class, () -> orderService.createOrder(validCommand) ); - + assertThat(exception.getMessage()).contains("database unavailable"); verify(eventService).createErrorEvent( eq(validCommand), @@ -125,7 +125,7 @@ class OrderServiceTest { NullPointerException.class, () -> orderService.createOrder(null) ); - + verify(orderRepository, never()).persist(any(Order.class)); } } @@ -184,7 +184,7 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class); mockRabbitMQ.expectedMessageCount(1); - + // Replace real endpoint with mock for testing camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { @@ -192,13 +192,13 @@ class BusinessRulesRouteTest { advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT — body is a JSON String after .marshal().json(JsonLibrary.Jackson) mockRabbitMQ.assertIsSatisfied(5000); - + assertThat(mockRabbitMQ.getExchanges()).hasSize(1); String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); @@ -211,19 +211,19 @@ class BusinessRulesRouteTest { MockEndpoint mockMarshal = new MockEndpoint("mock:marshal"); camelContext.addEndpoint("mock:marshal", mockMarshal); mockMarshal.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("business-rules-publisher"); AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> { advice.weaveAddLast().to("mock:marshal"); }); camelContext.getRouteController().startRoute("business-rules-publisher"); - + // ACT producerTemplate.sendBody("direct:business-rules-publisher", testPayload); - + // ASSERT mockMarshal.assertIsSatisfied(5000); - + String body = mockMarshal.getExchanges().get(0).getIn().getBody(String.class); assertThat(body).contains("\"documentId\":1"); assertThat(body).contains("\"flowProfile\":\"BASIC\""); @@ -240,17 +240,17 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockInvoice = camelContext.getEndpoint("mock:invoice", MockEndpoint.class); mockInvoice.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:process-invoice.*").replace().to("mock:invoice"); }); camelContext.getRouteController().startRoute("document-processing"); - + // ACT - producerTemplate.sendBodyAndHeader("direct:process-document", + producerTemplate.sendBodyAndHeader("direct:process-document", testPayload, "documentType", "INVOICE"); - + // ASSERT mockInvoice.assertIsSatisfied(5000); } @@ -261,23 +261,23 @@ class BusinessRulesRouteTest { // ARRANGE MockEndpoint mockError = camelContext.getEndpoint("mock:error", MockEndpoint.class); mockError.expectedMessageCount(1); - + camelContext.getRouteController().stopRoute("document-processing"); AdviceWith.adviceWith(camelContext, "document-processing", advice -> { advice.weaveByToString(".*direct:validation-error-handler.*") .replace().to("mock:error"); }); camelContext.getRouteController().startRoute("document-processing"); - + // Mock validator bean to throw exception when(documentValidator.validate(any())).thenThrow(new ValidationException("Invalid document")); - + // ACT producerTemplate.sendBody("direct:process-document", testPayload); - + // ASSERT mockError.assertIsSatisfied(5000); - + Exception exception = mockError.getExchanges().get(0).getException(); assertThat(exception).isInstanceOf(ValidationException.class); assertThat(exception.getMessage()).contains("Invalid document"); @@ -295,13 +295,13 @@ class EventServiceTest { @Mock private EventRepository eventRepository; - + @Mock private ObjectMapper objectMapper; - + @InjectMocks private EventService eventService; - + private BusinessRulesPayload testPayload; @BeforeEach @@ -314,19 +314,19 @@ class EventServiceTest { @Nested @DisplayName("Tests for createSuccessEvent") class CreateSuccessEvent { - + @Test @DisplayName("Should create success event with correct attributes") void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception { // ARRANGE when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED")); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("DOCUMENT_PROCESSED") && event.getStatus() == EventStatus.SUCCESS && event.getPayload().equals("{\"documentId\":1}") && @@ -339,13 +339,13 @@ class EventServiceTest { void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() { // ARRANGE Object nullPayload = null; - + // ACT & ASSERT NullPointerException exception = assertThrows( NullPointerException.class, () -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE") ); - + assertThat(exception.getMessage()).isEqualTo("Payload cannot be null"); verify(eventRepository, never()).persist(any()); } @@ -354,20 +354,20 @@ class EventServiceTest { @Nested @DisplayName("Tests for createErrorEvent") class CreateErrorEvent { - + @Test @DisplayName("Should create error event with error message") void givenError_whenCreateErrorEvent_thenEventPersistedWithMessage() throws Exception { // ARRANGE String errorMessage = "Processing failed"; when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}"); - + // ACT - assertDoesNotThrow(() -> + assertDoesNotThrow(() -> eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage)); - + // ASSERT - verify(eventRepository).persist(argThat(event -> + verify(eventRepository).persist(argThat(event -> event.getType().equals("PROCESSING_ERROR") && event.getStatus() == EventStatus.ERROR && event.getErrorMessage().equals(errorMessage) && @@ -384,7 +384,7 @@ class EventServiceTest { IllegalArgumentException.class, () -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage) ); - + assertThat(exception.getMessage()).contains("Error message cannot be blank"); } } @@ -400,13 +400,13 @@ class FileStorageServiceTest { @Mock private S3Client s3Client; - + @Mock private ExecutorService executorService; - + @InjectMocks private FileStorageService fileStorageService; - + private InputStream testInputStream; private LogContext testLogContext; @@ -421,7 +421,7 @@ class FileStorageServiceTest { @Nested @DisplayName("Tests for uploadOriginalFile") class UploadOriginalFile { - + @Test @DisplayName("Should successfully upload file and return document info") void givenValidFile_whenUpload_thenReturnsDocumentInfo() throws Exception { @@ -430,23 +430,23 @@ class FileStorageServiceTest { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenReturn(PutObjectResponse.builder().build()); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + StoredDocumentInfo result = future.join(); - + // ASSERT assertThat(result).isNotNull(); assertThat(result.getPath()).isNotBlank(); assertThat(result.getSize()).isEqualTo(1024L); assertThat(result.getUploadedAt()).isNotNull(); - + verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class)); } @@ -458,15 +458,15 @@ class FileStorageServiceTest { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) .thenThrow(new StorageException("S3 unavailable")); - + // ACT - CompletableFuture future = - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + CompletableFuture future = + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL); - + // ASSERT assertThatThrownBy(() -> future.join()) .isInstanceOf(CompletionException.class) @@ -479,17 +479,17 @@ class FileStorageServiceTest { void givenLogContext_whenUpload_thenContextPropagated() throws Exception { // ARRANGE AtomicReference capturedContext = new AtomicReference<>(); - + doAnswer(invocation -> { capturedContext.set(CustomLog.getCurrentContext()); ((Runnable) invocation.getArgument(0)).run(); return null; }).when(executorService).execute(any(Runnable.class)); - + // ACT - fileStorageService.uploadOriginalFile(testInputStream, 1024L, + fileStorageService.uploadOriginalFile(testInputStream, 1024L, testLogContext, InvoiceFormat.UBL).join(); - + // ASSERT assertThat(capturedContext.get()).isNotNull(); assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123"); @@ -641,7 +641,7 @@ class DocumentIntegrationTest { prepare-agent
- + report @@ -650,7 +650,7 @@ class DocumentIntegrationTest { report - + check @@ -705,14 +705,14 @@ mvn jacoco:check quarkus-junit5-mockito test - + org.mockito mockito-core test - + org.assertj @@ -720,14 +720,14 @@ mvn jacoco:check 3.24.2 test - + io.rest-assured rest-assured test - + org.apache.camel.quarkus diff --git a/skills/quarkus-verification/SKILL.md b/skills/quarkus-verification/SKILL.md index 54748e99..7b0194c9 100644 --- a/skills/quarkus-verification/SKILL.md +++ b/skills/quarkus-verification/SKILL.md @@ -437,28 +437,28 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Set up JDK 21 uses: actions/setup-java@v3 with: java-version: '21' distribution: 'temurin' - + - name: Cache Maven packages uses: actions/cache@v3 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - + - name: Build run: mvn clean verify -DskipTests - + - name: Test with Coverage run: mvn test jacoco:report jacoco:check - + - name: Security Scan run: mvn org.owasp:dependency-check-maven:check - + - name: Upload Coverage uses: codecov/codecov-action@v3 with: diff --git a/tests/ci/supply-chain-watch-workflow.test.js b/tests/ci/supply-chain-watch-workflow.test.js index b1388137..fa8198ae 100644 --- a/tests/ci/supply-chain-watch-workflow.test.js +++ b/tests/ci/supply-chain-watch-workflow.test.js @@ -43,7 +43,7 @@ function run() { if (test('uses read-only permissions and non-persisting checkout credentials', () => { assert.match(source, /permissions:\r?\n\s+contents: read/); assert.doesNotMatch(source, /^\s+[A-Za-z-]+:\s*write\b/m); - assert.match(source, /uses: actions\/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd/); + assert.match(source, /uses: actions\/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10/); assert.match(source, /persist-credentials: false/); assert.doesNotMatch(source, /id-token:\s*write/); assert.doesNotMatch(source, /actions\/cache@/);