mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-13 19:51:24 +08:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dce0d23c1 | |||
| ba00d0adeb | |||
| 0bc0546356 | |||
| 5df575bff8 | |||
| 6d539013ff | |||
| c93a772e63 | |||
| 3aab685277 | |||
| 1b3c967a7b | |||
| 51f2297581 | |||
| 37f2b32d69 | |||
| 7a4c25f1df | |||
| a8c03ad350 | |||
| a96787736d | |||
| a7699d04ba | |||
| 0e40ff640c | |||
| eebfd5dce2 | |||
| 1f50ab1903 | |||
| 68229a8996 | |||
| 8cbf6763c4 | |||
| de559bddd2 | |||
| 008ce3081b | |||
| cdf1b03779 | |||
| 969acd9078 | |||
| 60bd26fadf | |||
| cb2a70ce72 | |||
| f219a90f20 | |||
| 22aabf7d4f | |||
| 901e41997b | |||
| df6078ed1e | |||
| e17f2bcb1b | |||
| f8070dd640 | |||
| 11cc20bbcf | |||
| 46db568c38 | |||
| 408b262f11 | |||
| aca8dda78c | |||
| 53e2e798f7 | |||
| e3d4e33ddb | |||
| 80daa6dc78 | |||
| 6ed9b49a5b | |||
| 56bbbb3dbe | |||
| e928ceacee | |||
| c399627377 | |||
| eddfeb6fbf | |||
| 8f65048bc3 | |||
| 893eca0369 | |||
| 9b4704fe3d | |||
| ca7ff001ce | |||
| bc94f9926e | |||
| 4402e47553 | |||
| 8e5c5f59ce | |||
| eb2ea25b8e | |||
| 61dfbf8846 | |||
| e9089cf44e | |||
| 9b9f71c2e5 | |||
| 63934f382a | |||
| b54ce43ef3 | |||
| 08eb812da6 | |||
| 73de75abfd | |||
| 7945c3c979 | |||
| ddd611152e | |||
| c44d37e931 |
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"source": "./",
|
||||
"description": "The most comprehensive Claude Code plugin — 56 agents, 217 skills, 72 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
|
||||
"description": "The most comprehensive Claude Code plugin — 58 agents, 220 skills, 74 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
|
||||
"version": "2.0.0-rc.1",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.0.0-rc.1",
|
||||
"description": "Battle-tested Claude Code plugin for engineering teams — 56 agents, 217 skills, 72 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
|
||||
"description": "Battle-tested Claude Code plugin for engineering teams — 58 agents, 220 skills, 74 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"url": "https://x.com/affaanmustafa"
|
||||
|
||||
@@ -120,4 +120,6 @@ Remaining errors: 1
|
||||
|
||||
Final: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||
|
||||
For detailed Java and Spring Boot patterns, see `skill: springboot-patterns`.
|
||||
For detailed patterns and examples:
|
||||
- **Spring Boot**: See `skill: springboot-patterns`
|
||||
- **Quarkus**: See `skill: quarkus-patterns`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You are a senior Java engineer ensuring high standards of idiomatic Java and Spring Boot best practices.
|
||||
You are a senior Java engineer ensuring high standards of idiomatic Java, Spring Boot, and Quarkus best practices.
|
||||
|
||||
When invoked:
|
||||
1. Run `git diff -- '*.java'` to see recent Java file changes
|
||||
@@ -94,4 +94,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
||||
- **Warning**: MEDIUM issues only
|
||||
- **Block**: CRITICAL or HIGH issues found
|
||||
|
||||
For detailed Spring Boot patterns and examples, see `skill: springboot-patterns`.
|
||||
For detailed patterns and examples:
|
||||
- **Spring Boot**: See `skill: springboot-patterns`
|
||||
- **Quarkus**: See `skill: quarkus-patterns`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — Agent Instructions
|
||||
|
||||
This is a **production-ready AI coding plugin** providing 56 specialized agents, 217 skills, 72 commands, and automated hook workflows for software development.
|
||||
This is a **production-ready AI coding plugin** providing 58 specialized agents, 220 skills, 74 commands, and automated hook workflows for software development.
|
||||
|
||||
**Version:** 2.0.0-rc.1
|
||||
|
||||
@@ -147,9 +147,9 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
agents/ — 56 specialized subagents
|
||||
skills/ — 217 workflow skills and domain knowledge
|
||||
commands/ — 72 slash commands
|
||||
agents/ — 58 specialized subagents
|
||||
skills/ — 220 workflow skills and domain knowledge
|
||||
commands/ — 74 slash commands
|
||||
hooks/ — Trigger-based automations
|
||||
rules/ — Always-follow guidelines (common + per-language)
|
||||
scripts/ — Cross-platform Node.js utilities
|
||||
|
||||
@@ -358,7 +358,7 @@ If you stacked methods, clean up in this order:
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**That's it!** You now have access to 56 agents, 217 skills, and 72 legacy command shims.
|
||||
**That's it!** You now have access to 58 agents, 220 skills, and 74 legacy command shims.
|
||||
|
||||
### Dashboard GUI
|
||||
|
||||
@@ -456,7 +456,7 @@ everything-claude-code/
|
||||
| |-- plugin.json # Plugin metadata and component paths
|
||||
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add
|
||||
|
|
||||
|-- agents/ # 56 specialized subagents for delegation
|
||||
|-- agents/ # 58 specialized subagents for delegation
|
||||
| |-- planner.md # Feature implementation planning
|
||||
| |-- architect.md # System design decisions
|
||||
| |-- tdd-guide.md # Test-driven development
|
||||
@@ -522,14 +522,14 @@ everything-claude-code/
|
||||
| |-- laravel-verification/ # Laravel verification loops (NEW)
|
||||
| |-- python-patterns/ # Python idioms and best practices (NEW)
|
||||
| |-- python-testing/ # Python testing with pytest (NEW)
|
||||
| |-- quarkus-patterns/ # Java Quarkus patterns (NEW)
|
||||
| |-- quarkus-security/ # Quarkus security (NEW)
|
||||
| |-- quarkus-tdd/ # Quarkus TDD (NEW)
|
||||
| |-- quarkus-verification/ # Quarkus verification (NEW)
|
||||
| |-- springboot-patterns/ # Java Spring Boot patterns (NEW)
|
||||
| |-- springboot-security/ # Spring Boot security (NEW)
|
||||
| |-- springboot-tdd/ # Spring Boot TDD (NEW)
|
||||
| |-- springboot-verification/ # Spring Boot verification (NEW)
|
||||
| |-- quarkus-patterns/ # Quarkus REST, Panache, and messaging patterns (NEW)
|
||||
| |-- quarkus-security/ # Quarkus JWT/OIDC and RBAC security (NEW)
|
||||
| |-- quarkus-tdd/ # Quarkus testing with JUnit, REST Assured, and Dev Services (NEW)
|
||||
| |-- quarkus-verification/ # Quarkus build, test, security, and native verification (NEW)
|
||||
| |-- configure-ecc/ # Interactive installation wizard (NEW)
|
||||
| |-- security-scan/ # AgentShield security auditor integration (NEW)
|
||||
| |-- java-coding-standards/ # Java coding standards (NEW)
|
||||
@@ -856,7 +856,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/ecc/
|
||||
cp -r everything-claude-code/skills/search-first ~/.claude/skills/ecc/
|
||||
|
||||
# Optional: add niche/framework-specific skills only when needed
|
||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do
|
||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns quarkus-patterns; do
|
||||
# cp -r everything-claude-code/skills/$s ~/.claude/skills/ecc/
|
||||
# done
|
||||
|
||||
@@ -1360,9 +1360,9 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
||||
|
||||
| Feature | Claude Code | OpenCode | Status |
|
||||
|---------|-------------|----------|--------|
|
||||
| Agents | PASS: 56 agents | PASS: 12 agents | **Claude Code leads** |
|
||||
| Commands | PASS: 72 commands | PASS: 35 commands | **Claude Code leads** |
|
||||
| Skills | PASS: 217 skills | PASS: 37 skills | **Claude Code leads** |
|
||||
| Agents | PASS: 58 agents | PASS: 12 agents | **Claude Code leads** |
|
||||
| Commands | PASS: 74 commands | PASS: 35 commands | **Claude Code leads** |
|
||||
| Skills | PASS: 220 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** |
|
||||
@@ -1465,9 +1465,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 |
|
||||
|---------|------------|------------|-----------|----------|
|
||||
| **Agents** | 56 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
||||
| **Commands** | 72 | Shared | Instruction-based | 35 |
|
||||
| **Skills** | 217 | Shared | 10 (native format) | 37 |
|
||||
| **Agents** | 58 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
|
||||
| **Commands** | 74 | Shared | Instruction-based | 35 |
|
||||
| **Skills** | 220 | Shared | 10 (native format) | 37 |
|
||||
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
|
||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |
|
||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions |
|
||||
|
||||
+6
-2
@@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**完成!** 你现在可以使用 56 个代理、217 个技能和 72 个命令。
|
||||
**完成!** 你现在可以使用 58 个代理、220 个技能和 74 个命令。
|
||||
|
||||
### multi-* 命令需要额外配置
|
||||
|
||||
@@ -298,6 +298,10 @@ everything-claude-code/
|
||||
| |-- laravel-verification/ # Laravel 验证循环(新增)
|
||||
| |-- python-patterns/ # Python 惯用写法与最佳实践(新增)
|
||||
| |-- python-testing/ # 基于 pytest 的 Python 测试(新增)
|
||||
| |-- quarkus-patterns/ # Java Quarkus 模式(新增)
|
||||
| |-- quarkus-security/ # Quarkus 安全(新增)
|
||||
| |-- quarkus-tdd/ # Quarkus TDD(新增)
|
||||
| |-- quarkus-verification/ # Quarkus 验证(新增)
|
||||
| |-- springboot-patterns/ # Java Spring Boot 模式(新增)
|
||||
| |-- springboot-security/ # Spring Boot 安全(新增)
|
||||
| |-- springboot-tdd/ # Spring Boot TDD(新增)
|
||||
@@ -616,7 +620,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/
|
||||
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||
|
||||
# 可选:仅在需要时添加细分领域/框架专属技能
|
||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do
|
||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns quarkus-patterns; do
|
||||
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||
# done
|
||||
|
||||
|
||||
@@ -197,10 +197,12 @@ commands:
|
||||
- multi-plan
|
||||
- multi-workflow
|
||||
- plan
|
||||
- plan-prd
|
||||
- pm2
|
||||
- projects
|
||||
- promote
|
||||
- project-init
|
||||
- pr
|
||||
- prp-commit
|
||||
- prp-implement
|
||||
- prp-plan
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: homelab-architect
|
||||
description: Designs home and small-lab network plans from hardware inventory, goals, and operator experience level, with safe staged changes and rollback guidance.
|
||||
tools: ["Read", "Grep"]
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are a practical homelab network architect. Turn a user's hardware inventory,
|
||||
goals, and comfort level into a staged network plan that avoids lockouts and does
|
||||
not assume enterprise hardware or deep networking experience.
|
||||
|
||||
## Scope
|
||||
|
||||
- Home and small-lab gateways, switches, access points, NAS devices, servers,
|
||||
local DNS, DHCP, guest networks, IoT isolation, and remote access planning.
|
||||
- Planning and review only. Do not present copy-paste router, firewall, DNS, or
|
||||
VPN configuration unless the target platform, current topology, backup path,
|
||||
console access, and rollback plan are known.
|
||||
|
||||
Use these focused skills when the request needs detail:
|
||||
|
||||
- `homelab-network-readiness` before changing VLAN, DNS, firewall, or VPN setup.
|
||||
- `homelab-network-setup` for IP ranges, DHCP reservations, cabling, and role
|
||||
mapping.
|
||||
- `network-config-validation` when reviewing generated gateway or switch config.
|
||||
- `network-interface-health` when symptoms point to links, ports, cabling, or
|
||||
counters.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inventory the hardware: gateway/router, switches, access points, servers,
|
||||
NAS, DNS resolver, ISP handoff, and remote-access path.
|
||||
2. Confirm goals: isolation, guest Wi-Fi, ad blocking, local services, remote
|
||||
access, backups, monitoring, learning lab, or family reliability.
|
||||
3. Match goals to hardware capability. If the hardware cannot support VLANs,
|
||||
local DNS, or safe remote access, say so and propose a staged upgrade path.
|
||||
4. Design the smallest useful topology first, then optional later phases.
|
||||
5. Define rollback and access safety before any disruptive change.
|
||||
6. Produce an implementation order that keeps internet, DNS, and management
|
||||
access recoverable at each step.
|
||||
|
||||
## Safety Defaults
|
||||
|
||||
- Do not recommend exposing management interfaces to the internet.
|
||||
- Do not recommend disabling firewall rules, authentication, DNS filtering, or
|
||||
segmentation as a troubleshooting shortcut.
|
||||
- Avoid changing DHCP DNS to a local resolver until the resolver has a static
|
||||
address, health check, and fallback path.
|
||||
- Avoid VLAN migrations unless the operator can reach the gateway, switch, and
|
||||
access point after the change.
|
||||
- Prefer plain-English explanations and small reversible phases.
|
||||
|
||||
## Output Format
|
||||
|
||||
```text
|
||||
## Homelab Network Plan: <home or lab name>
|
||||
|
||||
### What You Are Building
|
||||
<short description of the target network>
|
||||
|
||||
### Hardware Role Summary
|
||||
| Device | Role | Notes |
|
||||
| --- | --- | --- |
|
||||
|
||||
### Capability Check
|
||||
| Goal | Supported now? | Requirement or upgrade |
|
||||
| --- | --- | --- |
|
||||
|
||||
### Addressing And Segmentation
|
||||
| Network | Purpose | Example range | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
### DNS, DHCP, And Local Services
|
||||
<resolver plan, static reservations, fallback, and service placement>
|
||||
|
||||
### Firewall And Access Rules
|
||||
- <plain-English rule>
|
||||
- <plain-English rule>
|
||||
|
||||
### Implementation Order
|
||||
1. <safe first step>
|
||||
2. <validation before next step>
|
||||
3. <rollback point>
|
||||
|
||||
### Quick Wins
|
||||
1. <small, high-value step>
|
||||
2. <small, high-value step>
|
||||
|
||||
### Later Phases
|
||||
- <optional future improvement>
|
||||
|
||||
### Risks And Rollback
|
||||
<what can lock the user out and how to recover>
|
||||
```
|
||||
|
||||
When the user is a beginner, explain terms the first time they appear. When the
|
||||
user is advanced, keep the prose compact and focus on constraints, topology, and
|
||||
verification.
|
||||
+124
-11
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: java-build-resolver
|
||||
description: Java/Maven/Gradle build, compilation, and dependency error resolution specialist. Fixes build errors, Java compiler errors, and Maven/Gradle issues with minimal changes. Use when Java or Spring Boot builds fail.
|
||||
description: Java/Maven/Gradle build, compilation, and dependency error resolution specialist. Automatically detects Spring Boot or Quarkus and applies framework-specific fixes. Fixes build errors, Java compiler errors, and Maven/Gradle issues with minimal changes. Use when Java builds fail.
|
||||
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
|
||||
model: sonnet
|
||||
---
|
||||
@@ -11,12 +11,25 @@ You are an expert Java/Maven/Gradle build error resolution specialist. Your miss
|
||||
|
||||
You DO NOT refactor or rewrite code — you fix the build error only.
|
||||
|
||||
## Framework Detection (run first)
|
||||
|
||||
Before attempting any fix, determine the framework:
|
||||
|
||||
```bash
|
||||
cat pom.xml 2>/dev/null || cat build.gradle 2>/dev/null || cat build.gradle.kts 2>/dev/null
|
||||
```
|
||||
|
||||
- If the build file contains `quarkus` → apply **[QUARKUS]** rules
|
||||
- If the build file contains `spring-boot` → apply **[SPRING]** rules
|
||||
- If both are present (unlikely) → flag as a finding and apply both rulesets
|
||||
- If neither is detected → use general Java rules only and note the ambiguity
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. Diagnose Java compilation errors
|
||||
2. Fix Maven and Gradle build configuration issues
|
||||
3. Resolve dependency conflicts and version mismatches
|
||||
4. Handle annotation processor errors (Lombok, MapStruct, Spring)
|
||||
4. Handle annotation processor errors (Lombok, MapStruct, Spring, Quarkus)
|
||||
5. Fix Checkstyle and SpotBugs violations
|
||||
|
||||
## Diagnostic Commands
|
||||
@@ -36,15 +49,18 @@ Run these in order:
|
||||
## Resolution Workflow
|
||||
|
||||
```text
|
||||
1. ./mvnw compile OR ./gradlew build -> Parse error message
|
||||
2. Read affected file -> Understand context
|
||||
3. Apply minimal fix -> Only what's needed
|
||||
4. ./mvnw compile OR ./gradlew build -> Verify fix
|
||||
5. ./mvnw test OR ./gradlew test -> Ensure nothing broke
|
||||
1. Detect framework (Spring Boot / Quarkus)
|
||||
2. ./mvnw compile OR ./gradlew build -> Parse error message
|
||||
3. Read affected file -> Understand context
|
||||
4. Apply minimal fix -> Only what's needed
|
||||
5. ./mvnw compile OR ./gradlew build -> Verify fix
|
||||
6. ./mvnw test OR ./gradlew test -> Ensure nothing broke
|
||||
```
|
||||
|
||||
## Common Fix Patterns
|
||||
|
||||
### General Java
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `cannot find symbol` | Missing import, typo, missing dependency | Add import or dependency |
|
||||
@@ -60,6 +76,34 @@ Run these in order:
|
||||
| `The following artifacts could not be resolved` | Private repo or network issue | Check repository credentials or `settings.xml` |
|
||||
| `COMPILATION ERROR: Source option X is no longer supported` | Java version mismatch | Update `maven.compiler.source` / `targetCompatibility` |
|
||||
|
||||
### [SPRING] Spring Boot Specific
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `No qualifying bean of type X` | Missing `@Component`/`@Service` or component scan | Add annotation or fix scan base package |
|
||||
| `Circular dependency involving X` | Constructor injection cycle | Refactor to break cycle or use `@Lazy` on one leg |
|
||||
| `BeanCreationException: Error creating bean` | Missing config, bad property, or missing dependency | Check `application.yml`, dependency tree |
|
||||
| `HttpMessageNotReadableException` | Malformed JSON or missing Jackson dependency | Check `spring-boot-starter-web` includes Jackson |
|
||||
| `Could not autowire. No beans of type found` | Missing bean or wrong profile active | Check `@Profile`, `@ConditionalOn*`, component scan |
|
||||
| `Failed to configure a DataSource` | Missing DB driver or datasource properties | Add driver dependency or `spring.datasource.*` config |
|
||||
| `spring-boot-starter-* not found` | BOM version mismatch | Check `spring-boot-dependencies` BOM version in parent |
|
||||
|
||||
### [QUARKUS] Quarkus Specific
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `UnsatisfiedResolutionException: no bean found` | Missing `@ApplicationScoped`/`@Inject` or missing extension | Add CDI annotation or `quarkus-*` extension |
|
||||
| `AmbiguousResolutionException` | Multiple beans match injection point | Add `@Priority`, `@Alternative`, or qualifier |
|
||||
| `Build step X threw an exception: RuntimeException` | Quarkus build-time augmentation failure | Read full stack trace — usually a missing extension, bad config, or reflection issue |
|
||||
| `Error injecting X: it's a non-proxyable bean type` | `@Singleton` with interceptor or `final` class | Switch to `@ApplicationScoped` or remove `final` |
|
||||
| `ClassNotFoundException at native image build` | Missing `@RegisterForReflection` or reflection config | Add `@RegisterForReflection` or `reflect-config.json` entry |
|
||||
| `BlockingNotAllowedOnIOThread` | Blocking call on Vert.x event loop | Add `@Blocking` to endpoint or use reactive client |
|
||||
| `ConfigurationException: SRCFG*` | Missing or malformed config property | Check `application.properties` for required `quarkus.*` or `mp.*` keys |
|
||||
| `quarkus-extension-* not found` | Wrong BOM version or extension not in BOM | Check `quarkus-bom` version; use `quarkus ext add <name>` |
|
||||
| `DEV mode hot reload failure` | Incompatible change during dev mode | Run `./mvnw quarkus:dev` with clean: `./mvnw clean quarkus:dev` |
|
||||
| `Panache entity not enhanced` | Entity not detected at build time | Ensure entity is in scanned package; check for missing `quarkus-hibernate-orm-panache` or `quarkus-mongodb-panache` extension |
|
||||
| `RESTEASY* deployment failure` | Duplicate JAX-RS paths or missing provider | Check `@Path` uniqueness; ensure `quarkus-resteasy-reactive` vs `quarkus-resteasy` are not mixed |
|
||||
|
||||
## Maven Troubleshooting
|
||||
|
||||
```bash
|
||||
@@ -108,10 +152,10 @@ java -version
|
||||
./gradlew -q javaToolchains
|
||||
```
|
||||
|
||||
## Spring Boot Specific
|
||||
## [SPRING] Spring Boot Specific Commands
|
||||
|
||||
```bash
|
||||
# Verify Spring Boot application context loads
|
||||
# Verify application context loads
|
||||
./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=test"
|
||||
|
||||
# Check for missing beans or circular dependencies
|
||||
@@ -119,6 +163,69 @@ java -version
|
||||
|
||||
# Verify Lombok is configured as annotation processor (not just dependency)
|
||||
grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
||||
|
||||
# Check Spring Boot version alignment
|
||||
./mvnw dependency:tree | grep "org.springframework.boot"
|
||||
```
|
||||
|
||||
## [QUARKUS] Quarkus Specific Commands
|
||||
|
||||
### Maven
|
||||
|
||||
```bash
|
||||
# Verify Quarkus build augmentation
|
||||
./mvnw quarkus:build -q
|
||||
|
||||
# Run in dev mode to surface runtime errors
|
||||
./mvnw quarkus:dev
|
||||
|
||||
# List installed extensions
|
||||
./mvnw quarkus:list-extensions -q 2>&1 | grep "✓\|installed"
|
||||
|
||||
# Add a missing extension
|
||||
./mvnw quarkus:add-extension -Dextensions="<extension-name>"
|
||||
|
||||
# Check Quarkus BOM version alignment
|
||||
./mvnw dependency:tree | grep "io.quarkus"
|
||||
|
||||
# Verify native build prerequisites (GraalVM)
|
||||
./mvnw package -Pnative -DskipTests 2>&1 | head -50
|
||||
|
||||
# Debug build-time augmentation failures
|
||||
./mvnw compile -X 2>&1 | grep -i "augment\|build step\|extension"
|
||||
```
|
||||
|
||||
### Gradle
|
||||
|
||||
```bash
|
||||
# Verify Quarkus build augmentation
|
||||
./gradlew quarkusBuild
|
||||
|
||||
# Run in dev mode to surface runtime errors
|
||||
./gradlew quarkusDev
|
||||
|
||||
# List installed extensions
|
||||
./gradlew listExtensions
|
||||
|
||||
# Add a missing extension
|
||||
./gradlew addExtension --extensions="<extension-name>"
|
||||
|
||||
# Check Quarkus dependency alignment
|
||||
./gradlew dependencies --configuration runtimeClasspath | grep "io.quarkus"
|
||||
|
||||
# Verify native build prerequisites (GraalVM)
|
||||
./gradlew build -Dquarkus.native.enabled=true -x test 2>&1 | head -50
|
||||
```
|
||||
|
||||
### Common (both build tools)
|
||||
|
||||
```bash
|
||||
# Check for reflection issues (native image)
|
||||
grep -rn "@RegisterForReflection" src/main/java --include="*.java"
|
||||
|
||||
# Verify CDI bean discovery (run dev mode first, then check output)
|
||||
# Maven: ./mvnw quarkus:dev | Gradle: ./gradlew quarkusDev
|
||||
# Then grep logs for: bean|unsatisfied|ambiguous
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
@@ -129,6 +236,8 @@ grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
||||
- **Always** run the build after each fix to verify
|
||||
- Fix root cause over suppressing symptoms
|
||||
- Prefer adding missing imports over changing logic
|
||||
- **[QUARKUS]**: Prefer `quarkus ext add` over manually editing `pom.xml` for extensions
|
||||
- **[QUARKUS]**: Always check if `@RegisterForReflection` is needed before adding reflection config manually
|
||||
- Check `pom.xml`, `build.gradle`, or `build.gradle.kts` to confirm the build tool before running commands
|
||||
|
||||
## Stop Conditions
|
||||
@@ -138,16 +247,20 @@ Stop and report if:
|
||||
- Fix introduces more errors than it resolves
|
||||
- Error requires architectural changes beyond scope
|
||||
- Missing external dependencies that need user decision (private repos, licences)
|
||||
- **[QUARKUS]**: Native image build fails due to GraalVM not being installed — report prerequisite
|
||||
|
||||
## Output Format
|
||||
|
||||
```text
|
||||
Framework: [SPRING|QUARKUS|BOTH|UNKNOWN]
|
||||
[FIXED] src/main/java/com/example/service/PaymentService.java:87
|
||||
Error: cannot find symbol — symbol: class IdempotencyKey
|
||||
Fix: Added import com.example.domain.IdempotencyKey
|
||||
Remaining errors: 1
|
||||
```
|
||||
|
||||
Final: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||
Final: `Framework: X | Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||
|
||||
For detailed Java and Spring Boot patterns, see `skill: springboot-patterns`.
|
||||
For detailed patterns and examples:
|
||||
- **[SPRING]**: See `skill: springboot-patterns`
|
||||
- **[QUARKUS]**: See `skill: quarkus-patterns`
|
||||
|
||||
+120
-32
@@ -1,65 +1,133 @@
|
||||
---
|
||||
name: java-reviewer
|
||||
description: Expert Java and Spring Boot code reviewer specializing in layered architecture, JPA patterns, security, and concurrency. Use for all Java code changes. MUST BE USED for Spring Boot projects.
|
||||
description: Expert Java code reviewer for Spring Boot and Quarkus projects. Automatically detects the framework and applies the appropriate review rules. Covers layered architecture, JPA/Panache, MongoDB, security, and concurrency. MUST BE USED for all Java code changes.
|
||||
tools: ["Read", "Grep", "Glob", "Bash"]
|
||||
model: sonnet
|
||||
---
|
||||
You are a senior Java engineer ensuring high standards of idiomatic Java and Spring Boot best practices.
|
||||
When invoked:
|
||||
You are a senior Java engineer ensuring high standards of idiomatic Java, Spring Boot, and Quarkus best practices.
|
||||
|
||||
## Framework Detection (run first)
|
||||
|
||||
Before reviewing any code, determine the framework:
|
||||
|
||||
```bash
|
||||
# Read the build file
|
||||
cat pom.xml 2>/dev/null || cat build.gradle 2>/dev/null || cat build.gradle.kts 2>/dev/null
|
||||
```
|
||||
|
||||
- If the build file contains `quarkus` → apply **[QUARKUS]** rules
|
||||
- If the build file contains `spring-boot` → apply **[SPRING]** rules
|
||||
- If both are present (unlikely) → flag as a finding and apply both rulesets
|
||||
- If neither is detected → review using general Java rules only and note the ambiguity
|
||||
|
||||
Then proceed:
|
||||
1. Run `git diff -- '*.java'` to see recent Java file changes
|
||||
2. Run `mvn verify -q` or `./gradlew check` if available
|
||||
2. Run the appropriate build check:
|
||||
- **[SPRING]**: `./mvnw verify -q` or `./gradlew check`
|
||||
- **[QUARKUS]**: `./mvnw verify -q` or `./gradlew check`
|
||||
3. Focus on modified `.java` files
|
||||
4. Begin review immediately
|
||||
|
||||
You DO NOT refactor or rewrite code — you report findings only.
|
||||
|
||||
---
|
||||
|
||||
## Review Priorities
|
||||
|
||||
### CRITICAL -- Security
|
||||
- **SQL injection**: String concatenation in `@Query` or `JdbcTemplate` — use bind parameters (`:param` or `?`)
|
||||
- **SQL injection**: String concatenation in queries — use bind parameters (`:param` or `?`)
|
||||
- **[SPRING]**: Watch for `@Query`, `JdbcTemplate`, `NamedParameterJdbcTemplate`
|
||||
- **[QUARKUS]**: Watch for `@Query`, Panache custom queries, `EntityManager.createNativeQuery()`
|
||||
- **Command injection**: User-controlled input passed to `ProcessBuilder` or `Runtime.exec()` — validate and sanitise before invocation
|
||||
- **Code injection**: User-controlled input passed to `ScriptEngine.eval(...)` — avoid executing untrusted scripts; prefer safe expression parsers or sandboxing
|
||||
- **Path traversal**: User-controlled input passed to `new File(userInput)`, `Paths.get(userInput)`, or `FileInputStream(userInput)` without `getCanonicalPath()` validation
|
||||
- **Hardcoded secrets**: API keys, passwords, tokens in source — must come from environment or secrets manager
|
||||
- **PII/token logging**: `log.info(...)` calls near auth code that expose passwords or tokens
|
||||
- **Missing `@Valid`**: Raw `@RequestBody` without Bean Validation — never trust unvalidated input
|
||||
- **CSRF disabled without justification**: Stateless JWT APIs may disable it but must document why
|
||||
- **Hardcoded secrets**: API keys, passwords, tokens in source
|
||||
- **[SPRING]**: Must come from environment, `application.yml`, or secrets manager (Vault, AWS Secrets Manager)
|
||||
- **[QUARKUS]**: Must come from `application.properties`, environment variables, or a secrets manager (e.g. `quarkus-vault`)
|
||||
- **PII/token logging**: Logging calls near auth code that expose passwords or tokens
|
||||
- **[SPRING]**: `log.info(...)` via SLF4J
|
||||
- **[QUARKUS]**: `Log.info(...)` or `@Logged` interceptors
|
||||
- **Missing input validation**: Request bodies accepted without Bean Validation
|
||||
- **[SPRING]**: Raw `@RequestBody` without `@Valid`
|
||||
- **[QUARKUS]**: Raw `@RestForm` / `@BeanParam` / request body without `@Valid` or `@ConvertGroup`
|
||||
- **CSRF disabled without justification**: Stateless JWT APIs may disable/omit it but must document why
|
||||
- **[QUARKUS]**: Form-based endpoints must use `quarkus-csrf-reactive`
|
||||
|
||||
If any CRITICAL security issue is found, stop and escalate to `security-reviewer`.
|
||||
|
||||
### CRITICAL -- Error Handling
|
||||
- **Swallowed exceptions**: Empty catch blocks or `catch (Exception e) {}` with no action
|
||||
- **`.get()` on Optional**: Calling `repository.findById(id).get()` without `.isPresent()` — use `.orElseThrow()`
|
||||
- **Missing `@RestControllerAdvice`**: Exception handling scattered across controllers instead of centralised
|
||||
- **`.get()` on Optional**: Calling `.get()` without `.isPresent()` — use `.orElseThrow()`
|
||||
- **[SPRING]**: `repository.findById(id).get()`
|
||||
- **[QUARKUS]**: `repository.findByIdOptional(id).get()`
|
||||
- **Missing centralised exception handling**:
|
||||
- **[SPRING]**: No `@RestControllerAdvice` — exception handling scattered across controllers
|
||||
- **[QUARKUS]**: No `ExceptionMapper<T>` or `@ServerExceptionMapper` — exception handling scattered across resources
|
||||
- **Wrong HTTP status**: Returning `200 OK` with null body instead of `404`, or missing `201` on creation
|
||||
|
||||
### HIGH -- Spring Boot Architecture
|
||||
- **Field injection**: `@Autowired` on fields is a code smell — constructor injection is required
|
||||
- **Business logic in controllers**: Controllers must delegate to the service layer immediately
|
||||
- **`@Transactional` on wrong layer**: Must be on service layer, not controller or repository
|
||||
- **Missing `@Transactional(readOnly = true)`**: Read-only service methods must declare this
|
||||
- **Entity exposed in response**: JPA entity returned directly from controller — use DTO or record projection
|
||||
### HIGH -- Architecture
|
||||
- **Dependency injection style**:
|
||||
- **[SPRING]**: `@Autowired` on fields is a code smell — constructor injection is required
|
||||
- **[QUARKUS]**: Bare field references expecting CDI — must use `@Inject` or constructor injection
|
||||
- **[QUARKUS] `@Singleton` vs `@ApplicationScoped`**: `@Singleton` beans are not proxied and break lazy initialization and interception — prefer `@ApplicationScoped` unless explicitly needed
|
||||
- **Business logic in controllers/resources**: Must delegate to the service layer immediately
|
||||
- **`@Transactional` on wrong layer**: Must be on service layer, not controller/resource or repository
|
||||
- **[SPRING]**: Missing `@Transactional(readOnly = true)` on read-only service methods
|
||||
- **[QUARKUS]**: Missing `@Transactional` on mutating Panache calls — active-record `persist()`, `delete()`, `update()` outside a transactional context will fail
|
||||
- **Entity exposed in response**: JPA/Panache entity returned directly from controller/resource — use DTO or record projection
|
||||
- **[QUARKUS] Blocking call on reactive thread**: Calling blocking I/O (JDBC, file I/O, `Thread.sleep()`) from a `@NonBlocking` endpoint or `Uni`/`Multi` pipeline — use `@Blocking`, `Uni.createFrom().item(() -> ...)` with `.runSubscriptionOn(executor)`, or the reactive client
|
||||
|
||||
### HIGH -- JPA / Database
|
||||
- **N+1 query problem**: `FetchType.EAGER` on collections — use `JOIN FETCH` or `@EntityGraph`
|
||||
- **Unbounded list endpoints**: Returning `List<T>` from endpoints without `Pageable` and `Page<T>`
|
||||
### HIGH -- JPA / Relational Database
|
||||
- **N+1 query problem**: `FetchType.EAGER` on collections — use `JOIN FETCH` or `@EntityGraph` / `@NamedEntityGraph`
|
||||
- **Unbounded list endpoints**:
|
||||
- **[SPRING]**: Returning `List<T>` without `Pageable` and `Page<T>`
|
||||
- **[QUARKUS]**: Returning `List<T>` without `PanacheQuery.page(Page.of(...))`
|
||||
- **Missing `@Modifying`**: Any `@Query` that mutates data requires `@Modifying` + `@Transactional`
|
||||
- **Dangerous cascade**: `CascadeType.ALL` with `orphanRemoval = true` — confirm intent is deliberate
|
||||
- **[QUARKUS] Active record misuse**: Mixing `PanacheEntity` and `PanacheRepository` in the same bounded context — pick one and stay consistent
|
||||
|
||||
### HIGH -- Panache MongoDB [QUARKUS only]
|
||||
- **Missing codec or serialisation config**: Custom types in documents without a registered `Codec` or proper BSON annotation — causes silent serialisation failures
|
||||
- **Unbounded `listAll()` / `findAll()`**: Using `PanacheMongoEntity.listAll()` or `PanacheMongoRepository.listAll()` without pagination — use `.find(query).page(Page.of(index, size))`
|
||||
- **No index on query fields**: Querying by fields not covered by a MongoDB index — define indexes via `@MongoEntity(collection = "...")` + migration scripts or `createIndex()` at startup
|
||||
- **ObjectId vs custom ID confusion**: Using `String` id fields without explicit `@BsonId` or `@MongoEntity` configuration — leads to `_id` mapping issues; prefer `ObjectId` or document the custom ID strategy
|
||||
- **Blocking MongoDB client on reactive thread**: Using the classic `MongoClient` (blocking) in a reactive pipeline — use `ReactiveMongoClient` and return `Uni<T>` / `Multi<T>`
|
||||
- **Active record misuse**: Mixing `PanacheMongoEntity` and `PanacheMongoRepository` in the same bounded context — pick one and stay consistent
|
||||
- **Missing `@Transactional` awareness**: MongoDB multi-document transactions require an explicit `ClientSession` — Panache MongoDB does not auto-manage transactions like Hibernate ORM; document the consistency guarantees
|
||||
|
||||
### MEDIUM -- NoSQL General
|
||||
- **Schema evolution without migration strategy**: Changing document shapes without a versioned migration plan (e.g. a `schemaVersion` field or migration script) — leads to runtime deserialization failures on old documents
|
||||
- **Storing large blobs in documents**: Embedding large binary data directly in documents instead of using GridFS or external storage — causes memory pressure and hits the 16 MB BSON limit
|
||||
- **Overly nested documents**: Deeply nested document structures that should be modelled as separate collections with references — query and update complexity grows exponentially
|
||||
- **Missing TTL or expiry policy**: Time-sensitive data (sessions, tokens, caches) stored without a TTL index — leads to unbounded collection growth
|
||||
- **No read preference / write concern configuration**: Production deployments using defaults without evaluating consistency requirements
|
||||
|
||||
### MEDIUM -- Concurrency and State
|
||||
- **Mutable singleton fields**: Non-final instance fields in `@Service` / `@Component` are a race condition
|
||||
- **Unbounded `@Async`**: `CompletableFuture` or `@Async` without a custom `Executor` — default creates unbounded threads
|
||||
- **Mutable singleton fields**: Non-final instance fields in singleton-scoped beans are a race condition
|
||||
- **[SPRING]**: `@Service` / `@Component`
|
||||
- **[QUARKUS]**: `@ApplicationScoped` / `@Singleton`
|
||||
- **Unbounded async execution**:
|
||||
- **[SPRING]**: `CompletableFuture` or `@Async` without a custom `Executor` — default creates unbounded threads
|
||||
- **[QUARKUS]**: `ExecutorService.submit()` or `@ActivateRequestContext` with `@Async` without a managed `ManagedExecutor`
|
||||
- **Blocking `@Scheduled`**: Long-running scheduled methods that block the scheduler thread
|
||||
- **[QUARKUS]**: Use `concurrentExecution = SKIP` or offload to a worker thread
|
||||
- **[QUARKUS] Reactive stream misuse**: Building `Uni`/`Multi` pipelines that subscribe more than once or share mutable state between subscribers
|
||||
|
||||
### MEDIUM -- Java Idioms and Performance
|
||||
- **String concatenation in loops**: Use `StringBuilder` or `String.join`
|
||||
- **Raw type usage**: Unparameterised generics (`List` instead of `List<T>`)
|
||||
- **Missed pattern matching**: `instanceof` check followed by explicit cast — use pattern matching (Java 16+)
|
||||
- **Null returns from service layer**: Prefer `Optional<T>` over returning null
|
||||
- **[QUARKUS] Not leveraging build-time init**: Using runtime reflection or classpath scanning that could be replaced by Quarkus build-time extensions or `@RegisterForReflection`
|
||||
|
||||
### MEDIUM -- Testing
|
||||
- **`@SpringBootTest` for unit tests**: Use `@WebMvcTest` for controllers, `@DataJpaTest` for repositories
|
||||
- **Missing Mockito extension**: Service tests must use `@ExtendWith(MockitoExtension.class)`
|
||||
- **Over-scoped test annotations**:
|
||||
- **[SPRING]**: `@SpringBootTest` for unit tests — use `@WebMvcTest` for controllers, `@DataJpaTest` for repositories
|
||||
- **[QUARKUS]**: `@QuarkusTest` for unit tests — reserve for integration tests; use plain JUnit 5 + Mockito for units
|
||||
- **Missing mock setup**:
|
||||
- **[SPRING]**: Service tests must use `@ExtendWith(MockitoExtension.class)`
|
||||
- **[QUARKUS]**: `@InjectMock` misuse — reserve for CDI integration tests, use plain Mockito for unit tests
|
||||
- **[QUARKUS] Missing `@QuarkusTestResource`**: Integration tests requiring external services should use Dev Services or `@QuarkusTestResource` with Testcontainers
|
||||
- **`Thread.sleep()` in tests**: Use `Awaitility` for async assertions
|
||||
- **Weak test names**: `testFindUser` gives no information — use `should_return_404_when_user_not_found`
|
||||
|
||||
@@ -68,25 +136,45 @@ If any CRITICAL security issue is found, stop and escalate to `security-reviewer
|
||||
- **Illegal state transitions**: No guard on transitions like `CANCELLED → PROCESSING`
|
||||
- **Non-atomic compensation**: Rollback/compensation logic that can partially succeed
|
||||
- **Missing jitter on retry**: Exponential backoff without jitter causes thundering herd
|
||||
- **[SPRING]**: Check Spring Retry configuration
|
||||
- **[QUARKUS]**: Check `@Retry` from MicroProfile Fault Tolerance
|
||||
- **No dead-letter handling**: Failed async events with no fallback or alerting
|
||||
- **[SPRING]**: Spring Kafka / AMQP error handlers
|
||||
- **[QUARKUS]**: SmallRye Reactive Messaging `@Incoming` dead-letter or `nack` strategy
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Commands
|
||||
|
||||
```bash
|
||||
# Common
|
||||
git diff -- '*.java'
|
||||
mvn verify -q
|
||||
./gradlew check # Gradle equivalent
|
||||
./mvnw checkstyle:check # style
|
||||
./mvnw spotbugs:check # static analysis
|
||||
./mvnw test # unit tests
|
||||
|
||||
# Build & verify
|
||||
./mvnw verify -q # Maven
|
||||
./gradlew check # Gradle
|
||||
|
||||
# Static analysis
|
||||
./mvnw checkstyle:check
|
||||
./mvnw spotbugs:check
|
||||
./mvnw dependency-check:check # CVE scan (OWASP plugin)
|
||||
grep -rn "@Autowired" src/main/java --include="*.java"
|
||||
|
||||
# Framework detection greps
|
||||
grep -rn "@Autowired" src/main/java --include="*.java" # [SPRING]
|
||||
grep -rn "@Inject" src/main/java --include="*.java" # [QUARKUS]
|
||||
grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
||||
grep -rn "@Singleton" src/main/java --include="*.java" # [QUARKUS]
|
||||
grep -rn "listAll\|findAll" src/main/java --include="*.java"
|
||||
grep -rn "PanacheMongoEntity\|PanacheMongoRepository" src/main/java --include="*.java" # [QUARKUS]
|
||||
```
|
||||
Read `pom.xml`, `build.gradle`, or `build.gradle.kts` to determine the build tool and Spring Boot version before reviewing.
|
||||
|
||||
Read `pom.xml`, `build.gradle`, or `build.gradle.kts` to determine the build tool and framework version before reviewing.
|
||||
|
||||
## Approval Criteria
|
||||
- **Approve**: No CRITICAL or HIGH issues
|
||||
- **Warning**: MEDIUM issues only
|
||||
- **Block**: CRITICAL or HIGH issues found
|
||||
|
||||
For detailed Spring Boot patterns and examples, see `skill: springboot-patterns`.
|
||||
For detailed patterns and examples:
|
||||
- **[SPRING]**: See `skill: springboot-patterns`
|
||||
- **[QUARKUS]**: See `skill: quarkus-patterns`
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: network-architect
|
||||
description: Designs enterprise or multi-site network architecture from requirements, using existing network skills for focused routing, validation, automation, and troubleshooting detail.
|
||||
tools: ["Read", "Grep"]
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are a senior network architecture planner. Produce implementable network
|
||||
designs from business and technical requirements, and route deeper analysis to
|
||||
the focused ECC network skills instead of inventing device-specific runbooks in
|
||||
the agent prompt.
|
||||
|
||||
## Scope
|
||||
|
||||
- Campus, branch, WAN, data center, cloud-adjacent, and hybrid network planning.
|
||||
- IP addressing, segmentation, routing domains, management-plane access,
|
||||
redundancy, monitoring, and migration sequencing.
|
||||
- Design and review only. Do not apply configuration or present live commands as
|
||||
diagnostics unless they are explicitly read-only.
|
||||
|
||||
Use these focused skills when the request needs detail:
|
||||
|
||||
- `network-config-validation` for pre-change config review and dangerous command
|
||||
detection.
|
||||
- `network-bgp-diagnostics` for BGP neighbor, route-policy, and prefix evidence.
|
||||
- `network-interface-health` for link, counter, CRC, drop, and flap analysis.
|
||||
- `cisco-ios-patterns` for IOS/IOS-XE syntax and safe show-command workflows.
|
||||
- `netmiko-ssh-automation` for bounded read-only network automation patterns.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Restate the objective, constraints, and non-goals.
|
||||
2. Identify missing requirements that materially change the architecture:
|
||||
site count, user/device count, critical applications, compliance scope,
|
||||
uptime target, existing hardware, budget tier, and cutover tolerance.
|
||||
3. Pick the topology and explain why it fits the constraints.
|
||||
4. Design routing and segmentation before discussing hardware.
|
||||
5. Define the management plane, logging, monitoring, backup, and rollback model.
|
||||
6. Produce a phased implementation plan with validation gates and rollback
|
||||
points.
|
||||
7. List residual risks and the evidence still needed from operators.
|
||||
|
||||
## Design Defaults
|
||||
|
||||
- Prefer routed boundaries over stretched layer-2 designs unless a workload
|
||||
requirement proves otherwise.
|
||||
- Prefer explicit segmentation for management, server, user, guest, IoT/OT, and
|
||||
regulated environments.
|
||||
- Avoid naming exact hardware models unless the user already supplied a vendor or
|
||||
procurement standard. Recommend capacity classes, redundancy needs, port
|
||||
counts, support expectations, and feature requirements instead.
|
||||
- Do not assume BGP, OSPF, EVPN, SD-WAN, or microsegmentation are required. Pick
|
||||
the simplest design that satisfies scale, operations, and risk.
|
||||
- Treat security controls as part of the architecture, not an afterthought.
|
||||
|
||||
## Output Format
|
||||
|
||||
```text
|
||||
## Network Architecture: <project or environment>
|
||||
|
||||
### Objective
|
||||
<what this design is for>
|
||||
|
||||
### Assumptions And Required Follow-Up
|
||||
- <assumption>
|
||||
- <question that would change the design>
|
||||
|
||||
### Recommended Topology
|
||||
<topology choice and reasoning>
|
||||
|
||||
### Addressing And Segmentation
|
||||
| Zone / domain | Purpose | Routing boundary | Allowed flows |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
### Routing And Connectivity
|
||||
<protocols, route boundaries, summarization, failover, and cloud/WAN notes>
|
||||
|
||||
### Management, Observability, And Backup
|
||||
<management access, logging, config backup, monitoring, and alerting>
|
||||
|
||||
### Implementation Phases
|
||||
1. <phase with validation gate>
|
||||
2. <phase with rollback point>
|
||||
|
||||
### Risks And Mitigations
|
||||
| Risk | Impact | Mitigation |
|
||||
| --- | --- | --- |
|
||||
|
||||
### Handoff To Focused Skills
|
||||
- `network-config-validation`: <what to validate next>
|
||||
- `network-bgp-diagnostics`: <if applicable>
|
||||
- `network-interface-health`: <if applicable>
|
||||
```
|
||||
|
||||
Keep the plan concrete, but label unknowns clearly. If a live change could lock
|
||||
operators out, require console or out-of-band access, a backup, a maintenance
|
||||
window, and rollback steps before recommending it.
|
||||
@@ -99,7 +99,7 @@ If PR not found, stop with error. Store PR metadata for later phases.
|
||||
Build review context:
|
||||
|
||||
1. **Project rules** — Read `CLAUDE.md`, `.claude/docs/`, and any contributing guidelines
|
||||
2. **PRP artifacts** — Check `.claude/PRPs/reports/` and `.claude/PRPs/plans/` for implementation context related to this PR
|
||||
2. **Planning artifacts** — Check `.claude/prds/`, `.claude/plans/`, `.claude/reviews/`, and legacy `.claude/PRPs/{prds,plans,reports,reviews}/` for context related to this PR
|
||||
3. **PR intent** — Parse PR description for goals, linked issues, test plans
|
||||
4. **Changed files** — List all modified files and categorize by type (source, test, config, docs)
|
||||
|
||||
@@ -188,7 +188,7 @@ Special cases:
|
||||
|
||||
### Phase 6 — REPORT
|
||||
|
||||
Create review artifact at `.claude/PRPs/reviews/pr-<NUMBER>-review.md`:
|
||||
Create review artifact at `.claude/reviews/pr-<NUMBER>-review.md` unless the repo already uses legacy `.claude/PRPs/reviews/` for this workstream:
|
||||
|
||||
```markdown
|
||||
# PR Review: #<NUMBER> — <TITLE>
|
||||
@@ -273,7 +273,7 @@ Issues: <critical_count> critical, <high_count> high, <medium_count> medium, <lo
|
||||
Validation: <pass_count>/<total_count> checks passed
|
||||
|
||||
Artifacts:
|
||||
Review: .claude/PRPs/reviews/pr-<NUMBER>-review.md
|
||||
Review: .claude/reviews/pr-<NUMBER>-review.md
|
||||
GitHub: <PR URL>
|
||||
|
||||
Next steps:
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
description: "Generate a lean, problem-first PRD and hand off to /plan for implementation planning."
|
||||
argument-hint: "[product/feature idea] (blank = start with questions)"
|
||||
---
|
||||
|
||||
# PRD Command
|
||||
|
||||
Produces a **Product Requirements Document** — the requirements-phase artifact of the SDLC. Captures *what* must be true for success and *why*, and stops before *how*. Implementation decomposition is delegated to `/plan`.
|
||||
|
||||
**Input**: `$ARGUMENTS`
|
||||
|
||||
## Scope of this command
|
||||
|
||||
| This command does | This command does NOT do |
|
||||
|---|---|
|
||||
| Frame the problem and users | Design the architecture |
|
||||
| Capture success criteria and scope | Pick files or write patterns |
|
||||
| List open questions and risks | Enumerate implementation tasks |
|
||||
| Write `.claude/prds/{name}.prd.md` | Produce an implementation plan — that's `/plan` |
|
||||
|
||||
If you find yourself writing implementation detail, stop and cut it. It belongs in `/plan`.
|
||||
|
||||
**Anti-fluff rule**: When information is missing, write `TBD — needs validation via {method}`. Never invent plausible-sounding requirements.
|
||||
|
||||
## Workflow
|
||||
|
||||
Four phases. Each phase is a single gate — ask the questions, wait for the user, then move on. No nested loops, no parallel research ceremony.
|
||||
|
||||
### Phase 1 — FRAME
|
||||
|
||||
If `$ARGUMENTS` is empty, ask:
|
||||
|
||||
> What do you want to build? One or two sentences.
|
||||
|
||||
If provided, restate in one sentence and ask:
|
||||
|
||||
> I understand: *{restated}*. Correct, or should I adjust?
|
||||
|
||||
Then ask the framing questions in a single set:
|
||||
|
||||
> 1. **Who** has this problem? (specific role or segment)
|
||||
> 2. **What** is the observable pain? (describe behavior, not assumed needs)
|
||||
> 3. **Why** can't they solve it with what exists today?
|
||||
> 4. **Why now?** — what changed that makes this worth doing?
|
||||
|
||||
Wait for the user. Do not proceed without answers (or explicit "skip").
|
||||
|
||||
### Phase 2 — GROUND
|
||||
|
||||
Ask for evidence. This is the shortest phase and the most load-bearing:
|
||||
|
||||
> What evidence do you have that this problem is real and worth solving? (user quotes, support tickets, metrics, observed behavior, failed workarounds — anything concrete)
|
||||
|
||||
If the user has none, record the PRD's Evidence section as `Assumption — needs validation via {user research | analytics | prototype}`. This keeps the PRD honest.
|
||||
|
||||
### Phase 3 — DECIDE
|
||||
|
||||
Scope and hypothesis in a single set:
|
||||
|
||||
> 1. **Hypothesis** — Complete: *We believe **{capability}** will **{solve problem}** for **{users}**. We'll know we're right when **{measurable outcome}**.*
|
||||
> 2. **MVP** — The minimum needed to test the hypothesis?
|
||||
> 3. **Out of scope** — What are you explicitly **not** building (even if users ask)?
|
||||
> 4. **Open questions** — Uncertainties that could change the approach?
|
||||
|
||||
Wait for responses.
|
||||
|
||||
### Phase 4 — GENERATE & HAND OFF
|
||||
|
||||
Create the directory if needed, write the PRD, and report.
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/prds
|
||||
```
|
||||
|
||||
**Output path**: `.claude/prds/{kebab-case-name}.prd.md`
|
||||
|
||||
#### PRD Template
|
||||
|
||||
```markdown
|
||||
# {Product / Feature Name}
|
||||
|
||||
## Problem
|
||||
{2–3 sentences: who has what problem, and what's the cost of leaving it unsolved?}
|
||||
|
||||
## Evidence
|
||||
- {User quote, data point, or observation}
|
||||
- {OR: "Assumption — needs validation via {method}"}
|
||||
|
||||
## Users
|
||||
- **Primary**: {role, context, what triggers the need}
|
||||
- **Not for**: {who this explicitly excludes}
|
||||
|
||||
## Hypothesis
|
||||
We believe **{capability}** will **{solve problem}** for **{users}**.
|
||||
We'll know we're right when **{measurable outcome}**.
|
||||
|
||||
## Success Metrics
|
||||
| Metric | Target | How measured |
|
||||
|---|---|---|
|
||||
| {primary} | {number} | {method} |
|
||||
|
||||
## Scope
|
||||
**MVP** — {the minimum to test the hypothesis}
|
||||
|
||||
**Out of scope**
|
||||
- {item} — {why deferred}
|
||||
|
||||
## Delivery Milestones
|
||||
<!-- Business outcomes, not engineering tasks. /plan turns each into a plan. -->
|
||||
<!-- Status: pending | in-progress | complete -->
|
||||
|
||||
| # | Milestone | Outcome | Status | Plan |
|
||||
|---|---|---|---|---|
|
||||
| 1 | {name} | {user-visible change} | pending | — |
|
||||
| 2 | {name} | {user-visible change} | pending | — |
|
||||
|
||||
## Open Questions
|
||||
- [ ] {question that could change scope or approach}
|
||||
|
||||
## Risks
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
|
||||
---
|
||||
*Status: DRAFT — requirements only. Implementation planning pending via /plan.*
|
||||
```
|
||||
|
||||
#### Report to user
|
||||
|
||||
```
|
||||
PRD created: .claude/prds/{name}.prd.md
|
||||
|
||||
Problem: {one line}
|
||||
Hypothesis: {one line}
|
||||
MVP: {one line}
|
||||
|
||||
Validation status:
|
||||
Problem {validated | assumption}
|
||||
Users {concrete | generic — refine}
|
||||
Metrics {defined | TBD}
|
||||
|
||||
Open questions: {count}
|
||||
|
||||
Next step: /plan .claude/prds/{name}.prd.md
|
||||
→ /plan will pick the next pending milestone and produce an implementation plan.
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
- `/plan <prd-path>` — consume the PRD and produce an implementation plan for the next pending milestone.
|
||||
- `tdd-workflow` skill — implement the plan test-first.
|
||||
- `/pr` — open a PR that references the PRD and plan.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- **PROBLEM_CLEAR**: problem is specific and evidenced (or flagged as assumption).
|
||||
- **USER_CONCRETE**: primary user is a specific role, not "users".
|
||||
- **HYPOTHESIS_TESTABLE**: measurable outcome included.
|
||||
- **SCOPE_BOUNDED**: explicit MVP and explicit out-of-scope.
|
||||
- **NO_IMPLEMENTATION_DETAIL**: file paths, libraries, or task breakdowns are absent — if they appeared, move them to the `/plan` step.
|
||||
+86
-7
@@ -1,10 +1,11 @@
|
||||
---
|
||||
description: Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code.
|
||||
argument-hint: "[feature description | path/to/*.prd.md]"
|
||||
---
|
||||
|
||||
# Plan Command
|
||||
|
||||
This command creates a comprehensive implementation plan before writing any code.
|
||||
This command creates a comprehensive implementation plan before writing any code. It accepts either free-form requirements or a PRD markdown file.
|
||||
|
||||
Run inline by default. Do not call the Task tool or any subagent by default. This keeps `/plan` usable from plugin installs that ship commands without agent files.
|
||||
|
||||
@@ -29,11 +30,86 @@ Use `/plan` when:
|
||||
The assistant will:
|
||||
|
||||
1. **Analyze the request** and restate requirements in clear terms
|
||||
2. **Break down into phases** with specific, actionable steps
|
||||
3. **Identify dependencies** between components
|
||||
4. **Assess risks** and potential blockers
|
||||
5. **Estimate complexity** (High/Medium/Low)
|
||||
6. **Present the plan** and WAIT for your explicit confirmation
|
||||
2. **Ground the plan** in relevant codebase patterns when the repo is available
|
||||
3. **Break down into phases** with specific, actionable steps
|
||||
4. **Identify dependencies** between components
|
||||
5. **Assess risks** and potential blockers
|
||||
6. **Estimate complexity** (High/Medium/Low)
|
||||
7. **Present the plan** and WAIT for your explicit confirmation
|
||||
|
||||
## Input Modes
|
||||
|
||||
| Input | Mode | Behavior |
|
||||
|---|---|---|
|
||||
| `path/to/name.prd.md` | PRD artifact mode | Read the PRD, pick the next pending delivery milestone or implementation phase, and write `.claude/plans/{name}.plan.md` |
|
||||
| Any other markdown path | Reference mode | Read the file as context and produce an inline plan |
|
||||
| Free-form text | Conversational mode | Produce an inline plan |
|
||||
| Empty input | Clarification mode | Ask what should be planned |
|
||||
|
||||
In PRD artifact mode, create `.claude/plans/` if needed. If the PRD contains a `Delivery Milestones` table, update only the selected row from `pending` to `in-progress` and set its `Plan` cell to the generated plan path. If the PRD uses the legacy `.claude/PRPs/prds/` format with `Implementation Phases`, read it without migrating paths.
|
||||
|
||||
## Pattern Grounding
|
||||
|
||||
Before writing the plan, search the codebase for conventions the implementation should mirror. Capture the top example for each relevant category with file references:
|
||||
|
||||
| Category | What to capture |
|
||||
|---|---|
|
||||
| Naming | File, function, type, command, or script naming in the affected area |
|
||||
| Error handling | How failures are raised, returned, logged, or handled gracefully |
|
||||
| Logging | Levels, format, and what gets logged |
|
||||
| Data access | Repository, service, query, or filesystem patterns |
|
||||
| Tests | Test file location, framework, fixtures, and assertion style |
|
||||
|
||||
If no similar code exists, state that explicitly. Do not invent a pattern.
|
||||
|
||||
## PRD Artifact Output
|
||||
|
||||
When called with a `.prd.md` file, write the plan to `.claude/plans/{kebab-case-name}.plan.md` using this structure:
|
||||
|
||||
````markdown
|
||||
# Plan: {Feature Name}
|
||||
|
||||
**Source PRD**: {path}
|
||||
**Selected Milestone**: {milestone or phase name}
|
||||
**Complexity**: {Small | Medium | Large}
|
||||
|
||||
## Summary
|
||||
{2-3 sentences}
|
||||
|
||||
## Patterns to Mirror
|
||||
| Category | Source | Pattern |
|
||||
|---|---|---|
|
||||
| Naming | `path:line` | {short description} |
|
||||
| Errors | `path:line` | {short description} |
|
||||
| Tests | `path:line` | {short description} |
|
||||
|
||||
## Files to Change
|
||||
| File | Action | Why |
|
||||
|---|---|---|
|
||||
| `path` | CREATE / UPDATE / DELETE | {reason} |
|
||||
|
||||
## Tasks
|
||||
### Task 1: {name}
|
||||
- **Action**: {what to do}
|
||||
- **Mirror**: {pattern to follow}
|
||||
- **Validate**: {command that proves correctness}
|
||||
|
||||
## Validation
|
||||
```bash
|
||||
{project-specific validation commands}
|
||||
```
|
||||
|
||||
## Risks
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
|
||||
## Acceptance
|
||||
- [ ] All tasks complete
|
||||
- [ ] Validation passes
|
||||
- [ ] Patterns mirrored, not reinvented
|
||||
````
|
||||
|
||||
After writing the artifact, report its path and WAIT for confirmation before writing code.
|
||||
|
||||
## Example Usage
|
||||
|
||||
@@ -108,8 +184,11 @@ After planning:
|
||||
- Use the `tdd-workflow` skill to implement with test-driven development
|
||||
- Use `/build-fix` if build errors occur
|
||||
- Use `/code-review` to review completed implementation
|
||||
- Use `/pr` or `/prp-pr` to open a pull request
|
||||
|
||||
> **Need deeper planning?** Use `/prp-plan` for artifact-producing planning with PRD integration, codebase analysis, and pattern extraction. Use `/prp-implement` to execute those plans with rigorous validation loops.
|
||||
> **Need requirements first?** Use `/plan-prd` for a lean PRD at `.claude/prds/{name}.prd.md`.
|
||||
>
|
||||
> **Need the legacy PRP flow?** Use `/prp-plan` for deep PRP planning with `.claude/PRPs/` artifacts. Use `/prp-implement` to execute those plans with rigorous validation loops.
|
||||
|
||||
## Optional Planner Agent
|
||||
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description: "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes"
|
||||
argument-hint: "[base-branch] (default: main)"
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
|
||||
**Input**: `$ARGUMENTS` — optional, may contain a base branch name and/or flags (e.g., `--draft`).
|
||||
|
||||
**Parse `$ARGUMENTS`**:
|
||||
- Extract any recognized flags (`--draft`)
|
||||
- Treat remaining non-flag text as the base branch name
|
||||
- Default base branch to `main` if none specified
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — VALIDATE
|
||||
|
||||
Check preconditions:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git status --short
|
||||
git log origin/<base>..HEAD --oneline
|
||||
```
|
||||
|
||||
| Check | Condition | Action if Failed |
|
||||
|---|---|---|
|
||||
| Not on base branch | Current branch ≠ base | Stop: "Switch to a feature branch first." |
|
||||
| Clean working directory | No uncommitted changes | Warn: "You have uncommitted changes. Commit or stash first." |
|
||||
| Has commits ahead | `git log origin/<base>..HEAD` not empty | Stop: "No commits ahead of `<base>`. Nothing to PR." |
|
||||
| No existing PR | `gh pr list --head <branch> --json number` is empty | Stop: "PR already exists: #<number>. Use `gh pr view <number> --web` to open it." |
|
||||
|
||||
If all checks pass, proceed.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — DISCOVER
|
||||
|
||||
### PR Template
|
||||
|
||||
Search for PR template in order:
|
||||
|
||||
1. `.github/PULL_REQUEST_TEMPLATE/` directory — if exists, list files and let user choose (or use `default.md`)
|
||||
2. `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
3. `.github/pull_request_template.md`
|
||||
4. `docs/pull_request_template.md`
|
||||
|
||||
If found, read it and use its structure for the PR body.
|
||||
|
||||
### Commit Analysis
|
||||
|
||||
```bash
|
||||
git log origin/<base>..HEAD --format="%h %s" --reverse
|
||||
```
|
||||
|
||||
Analyze commits to determine:
|
||||
- **PR title**: Use conventional commit format with type prefix — `feat: ...`, `fix: ...`, etc.
|
||||
- If multiple types, use the dominant one
|
||||
- If single commit, use its message as-is
|
||||
- **Change summary**: Group commits by type/area
|
||||
|
||||
### File Analysis
|
||||
|
||||
```bash
|
||||
git diff origin/<base>..HEAD --stat
|
||||
git diff origin/<base>..HEAD --name-only
|
||||
```
|
||||
|
||||
Categorize changed files: source, tests, docs, config, migrations.
|
||||
|
||||
### Planning Artifacts
|
||||
|
||||
Check for related artifacts produced by `/plan-prd`, `/plan`, or the legacy PRP workflow:
|
||||
- `.claude/prds/` — PRDs this PR implements a milestone of
|
||||
- `.claude/plans/` — Plans executed by this PR
|
||||
- `.claude/PRPs/prds/` — legacy PRP PRDs
|
||||
- `.claude/PRPs/plans/` — legacy PRP implementation plans
|
||||
- `.claude/PRPs/reports/` — legacy PRP implementation reports
|
||||
|
||||
Reference these in the PR body if they exist.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — PUSH
|
||||
|
||||
```bash
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
If push fails due to divergence:
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/<base>
|
||||
git push -u origin HEAD
|
||||
```
|
||||
|
||||
If rebase conflicts occur, stop and inform the user.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — CREATE
|
||||
|
||||
### With Template
|
||||
|
||||
If a PR template was found in Phase 2, fill in each section using the commit and file analysis. Preserve all template sections — leave sections as "N/A" if not applicable rather than removing them.
|
||||
|
||||
### Without Template
|
||||
|
||||
Use this default format:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
<1-2 sentence description of what this PR does and why>
|
||||
|
||||
## Changes
|
||||
|
||||
<bulleted list of changes grouped by area>
|
||||
|
||||
## Files Changed
|
||||
|
||||
<table or list of changed files with change type: Added/Modified/Deleted>
|
||||
|
||||
## Testing
|
||||
|
||||
<description of how changes were tested, or "Needs testing">
|
||||
|
||||
## Related Issues
|
||||
|
||||
<linked issues with Closes/Fixes/Relates to #N, or "None">
|
||||
```
|
||||
|
||||
### Create the PR
|
||||
|
||||
```bash
|
||||
gh pr create \
|
||||
--title "<PR title>" \
|
||||
--base <base-branch> \
|
||||
--body "<PR body>"
|
||||
# Add --draft if the --draft flag was parsed from $ARGUMENTS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — VERIFY
|
||||
|
||||
```bash
|
||||
gh pr view --json number,url,title,state,baseRefName,headRefName,additions,deletions,changedFiles
|
||||
gh pr checks --json name,status,conclusion 2>/dev/null || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — OUTPUT
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
PR #<number>: <title>
|
||||
URL: <url>
|
||||
Branch: <head> → <base>
|
||||
Changes: +<additions> -<deletions> across <changedFiles> files
|
||||
|
||||
CI Checks: <status summary or "pending" or "none configured">
|
||||
|
||||
Artifacts referenced:
|
||||
- <any PRDs/plans linked in PR body>
|
||||
|
||||
Next steps:
|
||||
- gh pr view <number> --web → open in browser
|
||||
- /code-review <number> → review the PR
|
||||
- gh pr merge <number> → merge when ready
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **No `gh` CLI**: Stop with: "GitHub CLI (`gh`) is required. Install: <https://cli.github.com/>"
|
||||
- **Not authenticated**: Stop with: "Run `gh auth login` first."
|
||||
- **Force push needed**: If remote has diverged and rebase was done, use `git push --force-with-lease` (never `--force`).
|
||||
- **Multiple PR templates**: If `.github/PULL_REQUEST_TEMPLATE/` has multiple files, list them and ask user to choose.
|
||||
- **Large PR (>20 files)**: Warn about PR size. Suggest splitting if changes are logically separable.
|
||||
@@ -0,0 +1,264 @@
|
||||
# ECC 2.0 GA Roadmap
|
||||
|
||||
This roadmap is the durable repo mirror for the Linear project:
|
||||
|
||||
<https://linear.app/ecctools/project/ecc-20-ga-harness-os-security-platform-de2a0ecace6f>
|
||||
|
||||
Linear issue creation is currently blocked by the workspace active issue limit,
|
||||
so the live execution truth is split across:
|
||||
|
||||
- the Linear project description, status updates, and milestones;
|
||||
- this repo document;
|
||||
- merged PR evidence;
|
||||
- handoffs under `~/.cluster-swarm/handoffs/`.
|
||||
|
||||
## Current Evidence
|
||||
|
||||
As of 2026-05-12:
|
||||
|
||||
- Public GitHub queues are clean across `everything-claude-code`,
|
||||
`agentshield`, `JARVIS`, `ECC-Tools`, and `ECC-website`.
|
||||
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
|
||||
- `npm run observability:ready` reports 14/14 readiness on current `main`.
|
||||
- `docs/architecture/harness-adapter-compliance.md` maps Claude Code, Codex,
|
||||
OpenCode, Cursor, Gemini, Zed-adjacent, dmux, Orca, Superset, Ghast, and
|
||||
terminal-only support to install paths, verification commands, and risk
|
||||
notes.
|
||||
- `npm run harness:adapters -- --check` validates that the public adapter
|
||||
matrix still matches the source data in
|
||||
`scripts/lib/harness-adapter-compliance.js`.
|
||||
- `docs/releases/2.0.0-rc.1/publication-readiness.md` gates GitHub release,
|
||||
npm dist-tag, Claude plugin, Codex plugin, OpenCode package, billing, and
|
||||
announcement publication on fresh evidence fields.
|
||||
- `docs/legacy-artifact-inventory.md` records that no `_legacy-documents-*`
|
||||
directories exist in the current checkout, inventories the two sibling
|
||||
workspace-level `_legacy-documents-*` repos as sanitized extraction sources,
|
||||
and classifies `legacy-command-shims/` as an opt-in archive/no-action
|
||||
surface.
|
||||
- `docs/stale-pr-salvage-ledger.md` records stale PR salvage outcomes,
|
||||
skipped PRs, superseded work, and the remaining #1687 translator/manual
|
||||
review tail.
|
||||
- AgentShield PR #53 reduced two context-rule false positives and closed the
|
||||
remaining AgentShield issues.
|
||||
- AgentShield PR #55 added GitHub Action organization-policy enforcement with
|
||||
`policy` / `fail-on-policy` inputs, `policy-status` /
|
||||
`policy-violations` outputs, job-summary evidence, and policy violation
|
||||
annotations.
|
||||
- AgentShield PR #56 added SARIF/code-scanning output for organization-policy
|
||||
violations as `agentshield-policy/*` results.
|
||||
- AgentShield PR #57 added OSS, team, enterprise, regulated,
|
||||
high-risk-hooks/MCP, and CI-enforcement policy-pack presets plus
|
||||
`agentshield policy init --pack`.
|
||||
- AgentShield PR #58 added MCP package provenance fields and report-level
|
||||
counts for npm vs git, pinned vs unpinned, known-good, and registry-backed
|
||||
supply-chain evidence.
|
||||
- AgentShield PR #59 added self-contained HTML executive summaries with risk
|
||||
posture, critical/high priority findings, category exposure, README/API
|
||||
docs, built-CLI smoke validation, and 1,704-test coverage.
|
||||
- AgentShield PR #60 added category-level built-in corpus benchmark output,
|
||||
a `readyForRegressionGate` signal, terminal `--corpus` category coverage,
|
||||
README/API docs, built-CLI smoke validation, and 1,705-test coverage.
|
||||
- ECC PR #1778 recovered the useful stale #1413 network/homelab architect-agent
|
||||
concepts.
|
||||
- ECC-Tools PR #26 added cost/token-risk predictive follow-ups for AI routing,
|
||||
Claude/model calls, usage limits, quota, and analysis-budget changes that lack
|
||||
budget, quota, rate-limit, or cost validation evidence.
|
||||
- ECC-Tools PR #27 added the non-blocking `ECC Tools / PR Risk Taxonomy`
|
||||
check-run for Security Evidence, Harness Drift, Install Manifest Integrity,
|
||||
CI/CD Recommendation, Cost/Token Risk, and Agent Config Review buckets.
|
||||
- ECC-Tools PR #28 added billing readiness audit checks for plan limits,
|
||||
entitlements, Marketplace plan shape, subscription source, seats, and
|
||||
overage metering.
|
||||
- ECC-Tools PR #29 added deterministic Reference Set Validation signals for
|
||||
analyzer, skill, agent, command, and harness-guidance changes that lack eval,
|
||||
golden trace, benchmark, or reference-set evidence.
|
||||
- ECC-Tools PR #30 capped follow-up generation to three new GitHub issues and
|
||||
one draft PR per run, then emits the remaining deterministic findings as a
|
||||
project sync backlog for Linear/status tracking without flooding trackers.
|
||||
- ECC-Tools PR #31 added review follow-up signals to analysis completion
|
||||
comments for outstanding change requests, unresolved or outdated review
|
||||
threads, and review activity without an explicit approval.
|
||||
- ECC-Tools PR #32 added CI failure-mode predictive follow-ups for workflow
|
||||
and test-runner changes that lack failure fixtures, captured logs,
|
||||
troubleshooting notes, dry-run evidence, or regression coverage.
|
||||
- ECC-Tools PR #33 added harness-config quality predictive follow-ups for MCP,
|
||||
plugin, agent, hook, command, and harness config changes that lack harness
|
||||
audit, adapter matrix, cross-harness docs, or compatibility regression
|
||||
evidence.
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Keep public PRs and issues below 20, with zero as the preferred release-lane
|
||||
target.
|
||||
- Maintain 70/70 harness audit and 14/14 observability readiness after every
|
||||
GA-readiness batch.
|
||||
- Do not publish release or social announcements until the GitHub release,
|
||||
npm/package state, billing state, and plugin submission surfaces are verified
|
||||
with fresh evidence.
|
||||
- Do not treat closed stale PRs as discarded. Pair each cleanup batch with a
|
||||
salvage pass: inspect the closed diffs, port useful compatible work on
|
||||
maintainer-owned branches, and credit the source PR.
|
||||
- Do not create new Linear issues until the active issue limit is cleared.
|
||||
|
||||
## Reference Pressure
|
||||
|
||||
The GA roadmap is informed by these reference surfaces:
|
||||
|
||||
- `stablyai/orca` and `superset-sh/superset` for worktree-native parallel agent
|
||||
UX, review loops, and workspace presets.
|
||||
- `standardagents/dmux` and `aidenybai/ghast` for terminal/worktree
|
||||
multiplexing, session grouping, and lifecycle hooks.
|
||||
- `jarrodwatts/claude-hud` for always-visible status, tool, agent, todo, and
|
||||
context telemetry.
|
||||
- `stanford-iris-lab/meta-harness` and `greyhaven-ai/autocontext` for
|
||||
evaluation-driven harness improvement, traces, playbooks, and promotion
|
||||
loops.
|
||||
- `NousResearch/hermes-agent` for operator shell, gateway, memory, skills, and
|
||||
multi-platform command patterns.
|
||||
- `anthropics/claude-code`, active `sst/opencode` / `anomalyco/opencode`, Zed,
|
||||
Codex, Cursor, Gemini, and terminal-only workflows for adapter expectations.
|
||||
|
||||
The output of this reference work should be concrete ECC deltas, not a second
|
||||
strategy memo.
|
||||
|
||||
## Milestones
|
||||
|
||||
### 1. GA Release, Naming, And Plugin Publication Readiness
|
||||
|
||||
Target: 2026-05-24
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Naming matrix covers product name, npm package, Claude plugin, Codex plugin,
|
||||
OpenCode package, marketplace metadata, docs, and migration copy.
|
||||
- GitHub release, npm dist-tag, plugin publication, and announcement gates are
|
||||
mapped to fresh command evidence.
|
||||
- Release notes, migration guide, known issues, quickstart, X thread, LinkedIn
|
||||
post, and GitHub release copy are ready but not posted before release URLs
|
||||
exist.
|
||||
- Plugin publication/contact paths for Claude and Codex are documented with
|
||||
owner, required artifacts, and submission status.
|
||||
|
||||
### 2. Harness Adapter Compliance Matrix And Scorecard Onramp
|
||||
|
||||
Target: 2026-05-31
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Adapter matrix covers Claude Code, Codex, OpenCode, Cursor, Gemini,
|
||||
Zed-adjacent surfaces, dmux, Orca, Superset, Ghast, and terminal-only use.
|
||||
- Each adapter has supported assets, unsupported surfaces, install path,
|
||||
verification command, and risk notes.
|
||||
- Harness audit remains 70/70 and gains a public onramp that explains how teams
|
||||
use the scorecard.
|
||||
- Reference findings are converted into concrete adapter, observability, or
|
||||
operator-surface deltas.
|
||||
|
||||
### 3. Local Observability, HUD/Status, And Session Control Plane
|
||||
|
||||
Target: 2026-06-07
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Observability readiness remains 14/14 and is backed by JSONL traces, status
|
||||
snapshots, risk ledger, and exportable handoff contracts.
|
||||
- HUD/status model covers context, tool calls, active agents, todos, checks,
|
||||
cost, risk, and queue state.
|
||||
- Worktree/session controls cover create, resume, status, stop, diff, PR,
|
||||
merge queue, and conflict queue.
|
||||
- Linear/GitHub/handoff sync model is explicit enough for real-time progress
|
||||
tracking.
|
||||
|
||||
### 4. Self-Improving Harness Evaluation Loop
|
||||
|
||||
Target: 2026-06-10
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Scenario specs, verifier contracts, traces, playbooks, and regression gates
|
||||
are documented and at least one read-only prototype exists.
|
||||
- The loop separates observation, proposal, verification, and promotion.
|
||||
- Team and individual setups can be scored and improved without blindly
|
||||
mutating configs.
|
||||
- RAG/reference-set design covers vetted ECC patterns, team history, CI
|
||||
failures, diffs, review outcomes, and harness config quality.
|
||||
|
||||
### 5. AgentShield Enterprise Security Platform
|
||||
|
||||
Target: 2026-06-14
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Formal policy schema exists for org baselines, exceptions, owners,
|
||||
expiration, severity, and audit trails.
|
||||
- SARIF/code-scanning output is implemented and tested.
|
||||
- GitHub Action policy gates expose organization policy status and violation
|
||||
counts for branch-protection and CI evidence.
|
||||
- Policy packs are defined for OSS, team, enterprise, regulated, high-risk
|
||||
hooks/MCP, and CI enforcement.
|
||||
- Supply-chain intelligence covers MCP package provenance and has an extension
|
||||
path for npm/pip reputation, CVEs, typosquats, and dependency risk.
|
||||
- Prompt-injection corpus and regression benchmark are ready for continuous
|
||||
rule hardening with category-level coverage and regression-gate output.
|
||||
- Enterprise reports include JSON plus self-contained HTML executive output
|
||||
with risk posture, priority findings, and category exposure.
|
||||
|
||||
### 6. ECC Tools Billing, Deep Analysis, PR Checks, And Linear Sync
|
||||
|
||||
Target: 2026-06-21
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Native GitHub Marketplace billing announcement is backed by verified
|
||||
implementation and docs.
|
||||
- Internal billing readiness audit covers plan limits, seats, entitlement
|
||||
mapping, Marketplace plan shape, subscription state, overage hooks, and
|
||||
failure modes.
|
||||
- Deep analyzer covers diff patterns, CI/CD workflows, dependency/security
|
||||
surface, PR review behavior, failure history, harness config, skill quality,
|
||||
and reference-set/RAG comparison.
|
||||
- PR check suite taxonomy includes Security Evidence, Harness Drift, Install
|
||||
Manifest Integrity, CI/CD Recommendation, Cost/Token Risk, and Agent Config
|
||||
Review.
|
||||
- Cost/token-risk predictive follow-ups flag AI routing, model-call, usage,
|
||||
quota, and budget changes when budget evidence is missing.
|
||||
- Reference-set validation follow-ups flag analyzer, skill, agent, command, and
|
||||
harness-guidance changes that lack eval, golden trace, benchmark, or
|
||||
maintained reference-set evidence.
|
||||
- PR analysis comments summarize review follow-up signals for requested
|
||||
changes, unresolved or outdated review threads, and missing approvals.
|
||||
- CI failure-mode predictive follow-ups flag workflow and test-runner changes
|
||||
that lack failure fixtures, captured logs, troubleshooting notes, dry-run
|
||||
evidence, or regression coverage.
|
||||
- Harness-config quality predictive follow-ups flag MCP, plugin, agent, hook,
|
||||
command, and harness config changes that lack audit, adapter matrix,
|
||||
cross-harness doc, or compatibility regression evidence.
|
||||
- Linear sync design maps findings to issues/status without flooding the
|
||||
workspace.
|
||||
- Follow-up generation caps automatic GitHub object creation and keeps overflow
|
||||
findings in a copy-ready project sync backlog.
|
||||
|
||||
### 7. Legacy Audit And Stale-Work Salvage Closure
|
||||
|
||||
Target: 2026-06-15
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Legacy directories and orphaned handoffs are inventoried.
|
||||
- Each useful artifact is marked landed, Linear/project-tracked, salvage
|
||||
branch, or archive/no-action.
|
||||
- Workspace-level legacy repos are mined only through sanitized maintainer
|
||||
branches; raw context, secrets, personal paths, local settings, and private
|
||||
drafts are never imported wholesale.
|
||||
- Stale PR salvage policy stays in force: close stale/conflicted PRs first,
|
||||
record a salvage ledger item, then port useful compatible content on
|
||||
maintainer branches with attribution.
|
||||
- #1687 localization leftovers are handled only by translator/manual review,
|
||||
not blind cherry-pick.
|
||||
|
||||
## Next Engineering Slices
|
||||
|
||||
1. Decide whether AgentShield PDF export adds value beyond the merged HTML
|
||||
executive report and corpus benchmark output.
|
||||
2. Extend ECC Tools deep analysis and Linear/project sync without flooding the
|
||||
workspace.
|
||||
@@ -1,54 +1,238 @@
|
||||
# ECC 2.0 Reference Architecture
|
||||
|
||||
Research summary from competitor/reference analysis (2026-03-22).
|
||||
Current execution mirror:
|
||||
[`ECC-2.0-GA-ROADMAP.md`](ECC-2.0-GA-ROADMAP.md).
|
||||
|
||||
## Competitive Landscape
|
||||
This document turns the May 2026 reference sweep into concrete ECC backlog
|
||||
shape. It is not a second strategy memo: every reference pressure below should
|
||||
land as an adapter, check, observable signal, security policy, PR review
|
||||
surface, or release-readiness gate.
|
||||
|
||||
| Project | Stars | Language | Type | Multi-Agent | Worktrees | Terminal-native |
|
||||
|---------|-------|----------|------|-------------|-----------|-----------------|
|
||||
| **ECC 2.0** | - | Rust | TUI | Yes | Yes | **Yes (SSH)** |
|
||||
| superset-sh/superset | 7.7K | TypeScript | Electron | Yes | Yes | No (desktop) |
|
||||
| standardagents/dmux | 1.2K | TypeScript | TUI (Ink) | Yes | Yes | Yes |
|
||||
| opencode-ai/opencode | 11.5K | Go | TUI | No | No | Yes |
|
||||
| smtg-ai/claude-squad | 6.5K | Go | TUI | Yes | Yes | Yes |
|
||||
## Reference Baseline
|
||||
|
||||
## Three-Layer Architecture
|
||||
Snapshot date: 2026-05-12.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ TUI Layer (ratatui) │ User-facing dashboard
|
||||
│ Panes, diff viewer, hotkeys │ Communicates via Unix socket
|
||||
├─────────────────────────────────┤
|
||||
│ Runtime Layer (library) │ Workspace runtime, agent registry,
|
||||
│ State persistence, detection │ status detection, SQLite
|
||||
├─────────────────────────────────┤
|
||||
│ Daemon Layer (process) │ Persistent across TUI restarts
|
||||
│ Terminal sessions, git ops, │ PTY management, heartbeats
|
||||
│ agent process supervision │
|
||||
└─────────────────────────────────┘
|
||||
| Reference | Primary pressure on ECC 2.0 | Concrete ECC delta |
|
||||
| --- | --- | --- |
|
||||
| [`stablyai/orca`](https://github.com/stablyai/orca) | Worktree-native multi-agent IDE with terminals, source control, GitHub integration, SSH, notifications, design/browser mode, account switching, and per-worktree context. | Treat worktree lifecycle, review state, notification state, and account/provider identity as first-class adapter signals. |
|
||||
| [`superset-sh/superset`](https://github.com/superset-sh/superset) | Desktop AI-agent workspace with parallel execution, worktree isolation, diff review, workspace presets, and broad CLI-agent compatibility. | Add workspace preset taxonomy and make ECC2 session/worktree state exportable enough for external editors to consume. |
|
||||
| [`standardagents/dmux`](https://github.com/standardagents/dmux) | Tmux/worktree orchestration, lifecycle hooks, multi-select agent control, smart merging, file browser, notifications, and cleanup. | Add lifecycle-hook coverage to the harness matrix and define merge/conflict queue events. |
|
||||
| [`aidenybai/ghast`](https://github.com/aidenybai/ghast) | Native macOS terminal multiplexer with cwd-grouped workspaces, panes, tabs, drag/drop, search, and notifications. | Preserve terminal-native ergonomics while adding cwd/session grouping and searchable handoff/session records. |
|
||||
| [`jarrodwatts/claude-hud`](https://github.com/jarrodwatts/claude-hud) | Always-visible Claude Code statusline for context, tools, agents, todos, and transcript-backed activity. | Formalize the ECC HUD/status payload for context, cost, tool calls, active agents, todos, queue state, checks, and risk. |
|
||||
| [`stanford-iris-lab/meta-harness`](https://github.com/stanford-iris-lab/meta-harness) | Automated search over task-specific harness design: what to store, retrieve, and show. | Split ECC improvement loops into scenario spec, proposer trace, verifier result, and promoted playbook. |
|
||||
| [`greyhaven-ai/autocontext`](https://github.com/greyhaven-ai/autocontext) | Recursive harness improvement using traces, reports, artifacts, datasets, playbooks, and role-separated evaluators. | Store reusable traces and playbooks before mutating installed harness assets. |
|
||||
| [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent) | Self-improving operator shell with memories, skills, scheduler, gateways, subagents, terminal backends, and migration tooling. | Keep ECC portable across local, SSH, container, and hosted terminal backends without hiding the underlying commands. |
|
||||
| [`anthropics/claude-code`](https://github.com/anthropics/claude-code), [`sst/opencode`](https://github.com/sst/opencode), Zed, Codex, Cursor, Gemini | Different agent harnesses expose different hooks, plugin surfaces, session stores, config files, and review loops. | Maintain a public adapter compliance matrix instead of treating one harness as the canonical UX. |
|
||||
| Local Claude Code source review | Session, tool, permission, hook, remote, analytics, task, and context-suggestion surfaces are more structured than the public CLI UX suggests. | Model status and risk events around session messages, permission requests, tool progress, context pressure, and summary state. |
|
||||
|
||||
## Architecture Shape
|
||||
|
||||
ECC 2.0 should be a harness operating system, not only a catalog of commands,
|
||||
agents, and skills.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Operator Surface │
|
||||
│ CLI, plugin, TUI, HUD/statusline, release gates, PR checks │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Harness Adapter Layer │
|
||||
│ Claude Code, Codex, OpenCode, Cursor, Gemini, Zed, dmux, │
|
||||
│ Orca, Superset, Ghast, terminal-only │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Worktree, Session, And Queue Runtime │
|
||||
│ worktrees, panes, sessions, todos, checks, merge/conflict │
|
||||
│ queues, notification state, ownership, handoff exports │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Observability And Evaluation Loop │
|
||||
│ JSONL traces, status snapshots, risk ledger, harness audit, │
|
||||
│ scenario specs, verifiers, promoted playbooks, RAG sets │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Security And Commercial Platform │
|
||||
│ AgentShield policies/SARIF, ECC Tools checks, billing, │
|
||||
│ Linear/GitHub sync, enterprise reports │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Patterns to Adopt
|
||||
## Reference-To-Backlog Map
|
||||
|
||||
### From Superset (Electron, 7.7K stars)
|
||||
- **Workspace Runtime Registry** — trait-based abstraction with capability flags
|
||||
- **Persistent daemon terminal** — sessions survive restarts via IPC
|
||||
- **Per-project mutex** for git operations (prevents race conditions)
|
||||
- **Port allocation** per workspace for dev servers
|
||||
- **Cold restore** from serialized terminal scrollback
|
||||
### Worktree And Session Orchestration
|
||||
|
||||
### From dmux (Ink TUI, 1.2K stars)
|
||||
- **Worker-per-pane status detection** — fingerprint terminal output + LLM classification
|
||||
- **Agent Registry** — centralized agent definitions (install check, launch cmd, permissions)
|
||||
- **Retry strategies** — different policies for destructive vs read-only operations
|
||||
- **PaneLifecycleManager** — exclusive locks preventing concurrent pane races
|
||||
- **Lifecycle hooks** — worktree_created, pre_merge, post_merge
|
||||
- **Background cleanup queue** — async worktree deletion
|
||||
Adopt from Orca, Superset, dmux, and Ghast:
|
||||
|
||||
## ECC 2.0 Advantages
|
||||
- Terminal-native (works over SSH, unlike Superset)
|
||||
- Integrates with 116-skill ecosystem
|
||||
- AgentShield security scanning
|
||||
- Self-improving skill evolution (continuous-learning-v2)
|
||||
- Rust single binary (3.4MB, no runtime deps)
|
||||
- First Rust-based agentic IDE TUI in open source
|
||||
- Worktree lifecycle events: create, resume, pause, stop, diff, review, PR,
|
||||
merge-ready, conflict, stale, close, salvage.
|
||||
- Session grouping by repo, branch, cwd, task, owner, and harness.
|
||||
- Workspace presets for release lane, PR triage lane, docs lane, security lane,
|
||||
and test-writer lane.
|
||||
- Notifications for blocked CI, dirty worktrees, merge conflicts, stale review,
|
||||
and finished autonomous runs.
|
||||
- Review loops that can annotate diffs and PRs without taking ownership away
|
||||
from maintainers.
|
||||
|
||||
Repo work:
|
||||
|
||||
- `everything-claude-code`: extend the adapter compliance matrix and public
|
||||
scorecard onramp.
|
||||
- `ecc2`: surface session/worktree state through a stable local payload before
|
||||
adding hosted telemetry.
|
||||
- `ECC-Tools`: consume the same lifecycle events for PR checks, issue routing,
|
||||
and Linear sync.
|
||||
|
||||
Verification:
|
||||
|
||||
- `npm run harness:audit -- --format json`
|
||||
- `npm run observability:ready`
|
||||
- targeted adapter matrix tests once the matrix moves from docs to data
|
||||
|
||||
### HUD, Status, And Observability
|
||||
|
||||
Adopt from Claude HUD and the Claude Code source review:
|
||||
|
||||
- Context pressure: usage, compaction risk, large-result warnings, and summary
|
||||
state.
|
||||
- Tool activity: active tool, recent tools, duration, risky operations, and
|
||||
permission requests.
|
||||
- Agent activity: active subagents, delegated task, branch/worktree, and wait
|
||||
state.
|
||||
- Queue activity: open PRs/issues, CI state, stale/conflict batches, review
|
||||
state, and closed-stale salvage backlog.
|
||||
- Cost/risk: token cost estimate, destructive-operation risk, hook/MCP risk,
|
||||
and security scan state.
|
||||
|
||||
Repo work:
|
||||
|
||||
- Keep `docs/architecture/observability-readiness.md` as the operator-facing
|
||||
readiness gate.
|
||||
- Define a versioned HUD/status JSON contract that both ECC2 and ECC Tools can
|
||||
consume.
|
||||
- Add sample exports from `loop-status`, `session-inspect`, harness audit, and
|
||||
risk ledger into a fixture directory before building visual UI.
|
||||
|
||||
Verification:
|
||||
|
||||
- `npm run observability:ready`
|
||||
- fixture validation for every status payload
|
||||
- cross-platform smoke test for commands that read session history
|
||||
|
||||
### Self-Improving Harness Loop
|
||||
|
||||
Adopt from Meta-Harness, Autocontext, and Hermes Agent:
|
||||
|
||||
- Separate the loop into observation, proposal, verification, promotion, and
|
||||
rollback.
|
||||
- Store every proposed improvement as trace plus artifact, not only as a final
|
||||
changed file.
|
||||
- Promote playbooks only after a verifier proves that they improve a scenario
|
||||
without widening blast radius.
|
||||
- Use RAG/reference sets for vetted ECC patterns, team history, CI failures,
|
||||
review outcomes, harness config quality, and security decisions.
|
||||
|
||||
Repo work:
|
||||
|
||||
- `everything-claude-code`: document scenario specs, verifier contracts, and
|
||||
playbook promotion rules.
|
||||
- `ECC-Tools`: map analyzer findings to PR comments, check runs, and Linear
|
||||
tasks without flooding the workspace.
|
||||
- `agentshield`: feed prompt-injection and config-risk findings into regression
|
||||
suites.
|
||||
|
||||
Verification:
|
||||
|
||||
- read-only prototype that emits a trace, report, candidate playbook, and
|
||||
verifier result
|
||||
- regression fixture proving a bad proposal is rejected
|
||||
|
||||
### AgentShield Enterprise Security Platform
|
||||
|
||||
AgentShield should move from useful scanner to enterprise security platform.
|
||||
|
||||
Backlog shape:
|
||||
|
||||
- Policy schema for org baseline, rule severity, owner, exception, expiration,
|
||||
evidence, and audit trail.
|
||||
- SARIF output for GitHub code scanning.
|
||||
- Policy packs for OSS, team, enterprise, regulated, high-risk hooks/MCP, and
|
||||
CI enforcement.
|
||||
- Supply-chain intelligence for MCP packages, npm/pip provenance, CVEs,
|
||||
typosquats, and dependency reputation.
|
||||
- Prompt-injection corpus and regression benchmark.
|
||||
- JSON plus executive HTML/PDF report output.
|
||||
|
||||
Verification:
|
||||
|
||||
- schema unit tests
|
||||
- SARIF fixture tests
|
||||
- policy-pack golden tests
|
||||
- false-positive regression tests from the public issue history
|
||||
|
||||
### ECC Tools Commercial And Review Platform
|
||||
|
||||
ECC Tools should become the GitHub-native layer for billing, deep analysis,
|
||||
PR checks, and Linear progress tracking.
|
||||
|
||||
Backlog shape:
|
||||
|
||||
- Native GitHub Marketplace billing audit before any payments announcement:
|
||||
plans, seats, org/account mapping, subscription state, overage behavior,
|
||||
downgrade/cancel behavior, and failure modes.
|
||||
- Deep analyzer comparable in scope to the useful parts of GitGuardian,
|
||||
Dependabot, CodeRabbit, and Greptile: security evidence, dependency risk,
|
||||
CI/CD recommendations, PR review behavior, config quality, token/cost risk,
|
||||
and harness drift.
|
||||
- RAG/reference set over vetted ECC patterns, historical PR outcomes,
|
||||
dependency advisories, CI failures, review decisions, and team-specific
|
||||
conventions.
|
||||
- Linear sync that maps findings to project status, milestone evidence, and
|
||||
owner-ready issues without exhausting issue limits.
|
||||
|
||||
Verification:
|
||||
|
||||
- check-run fixture tests
|
||||
- billing webhook replay tests
|
||||
- analyzer golden PR fixtures
|
||||
- Linear sync dry-run fixture
|
||||
|
||||
### Closed-Stale Salvage Lane
|
||||
|
||||
Closing stale PRs keeps the public queue usable, but useful work should not be
|
||||
lost because a contributor no longer has time to rebase.
|
||||
|
||||
Execution rule:
|
||||
|
||||
1. Close stale, conflicted, or obsolete PRs with a clear courtesy comment.
|
||||
2. Record them in a salvage ledger with source PR, author, reason closed,
|
||||
useful files/concepts, risk, and recommended maintainer action.
|
||||
3. After the cleanup batch, inspect each closed PR diff manually.
|
||||
4. Cherry-pick only when the patch still applies cleanly and preserves current
|
||||
architecture. Otherwise reimplement the useful idea in a fresh maintainer
|
||||
branch.
|
||||
5. Preserve attribution in the commit body or PR body.
|
||||
6. Comment back on the source PR when useful work lands, linking the maintainer
|
||||
PR or merged commit.
|
||||
7. Mark the ledger item as landed, superseded, Linear-tracked, or no-action.
|
||||
|
||||
Required safeguards:
|
||||
|
||||
- Never blind cherry-pick generated churn, bulk localization, or dependency
|
||||
major-version changes.
|
||||
- Prefer small maintainer PRs over one salvage megabranch.
|
||||
- Run the same validation gates as normal code, docs, or catalog changes.
|
||||
- Keep contributor credit even when the final implementation is rewritten.
|
||||
|
||||
## Near-Term Implementation Order
|
||||
|
||||
1. Extend the harness adapter matrix and public scorecard onramp.
|
||||
2. Add the release/name/plugin publication checklist with evidence fields.
|
||||
3. Define the HUD/status JSON contract and fixture directory.
|
||||
4. Start AgentShield policy schema plus SARIF fixtures.
|
||||
5. Audit ECC Tools billing and check-run surfaces.
|
||||
6. Inventory legacy folders and closed-stale PRs into the salvage ledger.
|
||||
7. Port useful stale work in small attributed maintainer PRs.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Hosted telemetry before the local event model is useful and testable.
|
||||
- Automatic mutation of user harness configs without verifier evidence.
|
||||
- Treating any one agent harness as the canonical interface.
|
||||
- Release or payments announcements before command, package, marketplace, and
|
||||
billing evidence is fresh.
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# Plan-PRD Pattern: Markdown-Staged Planning Flow
|
||||
|
||||
A lightweight, SDLC-aligned planning workflow where each phase of the lifecycle produces a committable markdown **staging file** that the next command consumes.
|
||||
|
||||
> Short version: `/plan-prd` writes a PRD, `/plan` writes a plan, the `tdd-workflow` skill implements it, and `/pr` ships it. Each arrow is a file on disk, not a conversation in memory.
|
||||
|
||||
## Feature: Markdown Staging Files
|
||||
|
||||
Every planning artifact is a plain `.md` file under `.claude/`:
|
||||
|
||||
```
|
||||
.claude/
|
||||
prds/ # Product Requirements Documents from /plan-prd
|
||||
plans/ # Implementation plans from /plan
|
||||
reviews/ # Code review artifacts from /code-review
|
||||
```
|
||||
|
||||
These files are:
|
||||
|
||||
- **Plain markdown** — readable by humans, diffable in PRs, grep-able at CLI.
|
||||
- **Committable** — check them in alongside code so the intent travels with the implementation.
|
||||
- **Composable** — each command accepts the previous stage's file as its `$ARGUMENTS`, so the toolchain composes via paths rather than in-context state.
|
||||
- **Resumable** — close the session, open a new one tomorrow, pass the file path back in.
|
||||
|
||||
## Flow
|
||||
|
||||
```
|
||||
┌───────────────────────────┐
|
||||
│ /plan-prd "<idea>" │ Requirements phase
|
||||
│ → .claude/prds/X.prd.md │ Problem · Users · Hypothesis · Scope
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────┐
|
||||
│ /plan <prd-path> │ Design phase
|
||||
│ → .claude/plans/X.plan.md│ Patterns · Files · Tasks · Validation
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────┐
|
||||
│ tdd-workflow skill │ Implementation phase
|
||||
│ → code + tests │ Test-first, minimal diff
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────┐
|
||||
│ /pr │ Delivery phase
|
||||
│ → GitHub PR │ Links back to PRD + plan
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
Each box is a **gate**. You can:
|
||||
|
||||
- Stop between gates — the artifact persists.
|
||||
- Restart from any gate using the artifact path.
|
||||
- Skip gates for small work — feed `/plan` free-form text and ignore `/plan-prd`.
|
||||
- Run a gate standalone — `/plan "refactor X"` produces a conversational plan with no artifact.
|
||||
|
||||
## Why `/plan-prd` Is Additional to `/plan`
|
||||
|
||||
They answer different questions. Mixing them causes scope creep.
|
||||
|
||||
| Command | Answers | SDLC Phase | Artifact |
|
||||
|---|---|---|---|
|
||||
| `/plan-prd` | *What problem? For whom? How do we know we're done?* | Requirements | `.claude/prds/{name}.prd.md` |
|
||||
| `/plan` | *What files, patterns, and tasks satisfy the requirement?* | Design + Implementation strategy | `.claude/plans/{name}.plan.md` (PRD mode) or inline (text mode) |
|
||||
|
||||
### Why not combine them?
|
||||
|
||||
- **Separation of concerns.** PRDs ask *why*; plans ask *how*. Bundling them creates one oversized command that does both poorly, as the old `/prp-prd` → `/prp-plan` pair demonstrated (8-phase interrogation with implementation-phase tables mixed into requirements).
|
||||
- **Different audiences.** A stakeholder reviewing a PRD does not care about file paths or type-check commands. An engineer reading a plan does not need the market-research phase.
|
||||
- **Different lifespans.** A PRD can remain stable while its plan is rewritten multiple times as implementation assumptions change.
|
||||
- **Optional step.** Many changes (bug fixes, small refactors, single-file additions) don't need a PRD. `/plan` alone is enough. Forcing a PRD on every change is bureaucracy.
|
||||
|
||||
### When to use each
|
||||
|
||||
Use `/plan-prd` when:
|
||||
|
||||
- Scope is unclear or contested.
|
||||
- Multiple stakeholders need to align on the problem before solutioning.
|
||||
- The change is large enough that writing down the hypothesis is cheaper than relitigating scope mid-implementation.
|
||||
|
||||
Use `/plan` directly when:
|
||||
|
||||
- Requirements are already clear (a bug report, a scoped refactor, a known migration).
|
||||
- The work is small enough that a conversational plan + confirmation gate is sufficient.
|
||||
- You already have a PRD — pass it to `/plan` and skip `/plan-prd`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Full flow (feature with unclear scope)
|
||||
|
||||
```bash
|
||||
# 1. Draft the PRD
|
||||
/plan-prd "Per-user rate limits on the public API"
|
||||
|
||||
# → .claude/prds/per-user-rate-limits.prd.md created
|
||||
# Answer the framing questions, provide evidence, define hypothesis and scope.
|
||||
|
||||
# 2. Pick the next pending milestone and produce a plan
|
||||
/plan .claude/prds/per-user-rate-limits.prd.md
|
||||
|
||||
# → .claude/plans/per-user-rate-limits.plan.md created
|
||||
# The plan includes patterns to mirror, files to change, and validation commands.
|
||||
# PRD's Delivery Milestones table updates the selected row to `in-progress`.
|
||||
|
||||
# 3. Implement test-first
|
||||
Use the tdd-workflow skill
|
||||
|
||||
# 4. Open the PR
|
||||
/pr
|
||||
# → PR body auto-references .claude/prds/... and .claude/plans/...
|
||||
```
|
||||
|
||||
### Quick flow (scope already clear)
|
||||
|
||||
```bash
|
||||
/plan "Add retry with exponential backoff to the notifier"
|
||||
# Conversational planning, no artifact.
|
||||
# Confirm, then use the tdd-workflow skill.
|
||||
```
|
||||
|
||||
### Reference an existing PRD from elsewhere
|
||||
|
||||
```bash
|
||||
# PRD was written by someone else, lives in your repo
|
||||
/plan docs/rfcs/0042-rate-limiting.prd.md
|
||||
```
|
||||
|
||||
`/plan` detects any `.prd.md` path and switches to artifact mode, parsing the Delivery Milestones table.
|
||||
|
||||
## Why staging files beat in-context state
|
||||
|
||||
- **Transferable**: drop the PRD path into a fresh session and you're caught up — no replaying a long conversation.
|
||||
- **Auditable**: the PR reviewer sees *what you intended* next to *what you built*.
|
||||
- **Versioned**: the staging file evolves in git history, same as code.
|
||||
- **Machine-parseable**: `/plan` programmatically picks the next pending milestone; `/pr` programmatically links artifacts in the PR body. No prompt engineering required.
|
||||
|
||||
## Related commands
|
||||
|
||||
- `/plan-prd` — requirements (this pattern entry point).
|
||||
- `/plan` — planning (consumes PRDs or free-form text).
|
||||
- `tdd-workflow` skill — test-first implementation.
|
||||
- `/pr` — open a PR that references PRDs and plans.
|
||||
- `/code-review` — reviews local diffs or PRs; auto-detects `.claude/prds/` and `.claude/plans/` as context.
|
||||
|
||||
## Compatibility
|
||||
|
||||
This pattern adds ECC-native staging-file commands alongside the existing `prp-*` command set. The legacy PRP commands remain available for deeper PRP workflows and for users who already have `.claude/PRPs/` artifacts.
|
||||
|
||||
- `/plan-prd` is the lean requirements entry point for `.claude/prds/`.
|
||||
- `/plan` can consume `.prd.md` files and produce `.claude/plans/` artifacts without requiring the legacy PRP directory layout.
|
||||
- `/pr` is the ECC-native PR creation command and can reference `.claude/prds/` and `.claude/plans/`.
|
||||
- `/prp-prd`, `/prp-plan`, `/prp-implement`, `/prp-commit`, and `/prp-pr` remain valid legacy/deep workflow commands.
|
||||
@@ -13,6 +13,9 @@ The goal is to keep the durable parts of agentic work in one repo:
|
||||
|
||||
Claude Code, Codex, OpenCode, Cursor, Gemini, and future harnesses should adapt those assets at the edge instead of requiring a new workflow model for every tool.
|
||||
|
||||
For the operator-facing support matrix and scorecard workflow, see
|
||||
[Harness Adapter Compliance Matrix](harness-adapter-compliance.md).
|
||||
|
||||
## Portability Model
|
||||
|
||||
| Surface | Shared Source | Harness Adapter | Current Status |
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Harness Adapter Compliance Matrix
|
||||
|
||||
This matrix is the public onramp for teams that want to use ECC across more
|
||||
than one coding harness. It turns the cross-harness architecture into a
|
||||
practical scorecard: what works today, what is instruction-only, what needs an
|
||||
adapter, and what evidence an operator should collect before trusting a setup.
|
||||
|
||||
ECC's durable units stay in shared sources:
|
||||
|
||||
- `skills/*/SKILL.md`
|
||||
- `rules/`
|
||||
- `commands/`
|
||||
- `hooks/hooks.json`
|
||||
- `scripts/hooks/`
|
||||
- MCP reference configs
|
||||
- session and observability contracts
|
||||
|
||||
Harness-specific files should only adapt loading, event shape, command names,
|
||||
or platform limits.
|
||||
|
||||
## Compliance States
|
||||
|
||||
| State | Meaning |
|
||||
| --- | --- |
|
||||
| Native | ECC can install or verify the surface directly for this harness. |
|
||||
| Adapter-backed | ECC has a thin adapter, plugin, or package surface, but parity differs by harness. |
|
||||
| Instruction-backed | ECC can provide the guidance and files, but the harness does not expose the runtime hook/session surface ECC needs for enforcement. |
|
||||
| Reference-only | The tool is useful as a design pressure or external runtime, but ECC does not yet ship a direct installer or adapter for it. |
|
||||
|
||||
## Matrix
|
||||
|
||||
The matrix below is rendered from
|
||||
`scripts/lib/harness-adapter-compliance.js` and verified by
|
||||
`npm run harness:adapters -- --check`.
|
||||
|
||||
<!-- harness-adapter-compliance:matrix-start -->
|
||||
| Harness or runtime | State | Supported assets | Unsupported or different surfaces | Install or onramp | Verification command | Risk notes |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Claude Code | Native | Claude plugin assets; skills; commands; hooks; MCP config; local rules; statusline-oriented workflows | Claude-native hooks do not imply parity in other harnesses | `./install.sh --profile minimal --target claude`; Claude plugin install | `npm run harness:audit -- --format json`; `node scripts/session-inspect.js --list-adapters` | Avoid loading every skill by default; keep hooks opt-in and inspectable. |
|
||||
| Codex | Instruction-backed | `AGENTS.md`; Codex plugin metadata; skills; MCP reference config; command patterns | Native hook enforcement and Claude slash-command semantics are not equivalent | `./install.sh --profile minimal --target codex`; repo-local `AGENTS.md` review | `npm run harness:audit -- --format json` | Treat hooks as policy text unless a native Codex hook surface exists. |
|
||||
| OpenCode | Adapter-backed | OpenCode package/plugin metadata; shared skills; MCP config; event adapter patterns | Event names, plugin packaging, and command dispatch differ from Claude Code | OpenCode package or plugin surface from this repo | `node tests/scripts/build-opencode.test.js`; `npm run harness:audit -- --format json` | Keep hook logic in shared scripts and adapt only event shape at the edge. |
|
||||
| Cursor | Adapter-backed | Cursor rules; project-local skills; hook adapter; shared scripts | Cursor hook events and rule loading differ from Claude Code | `./install.sh --profile minimal --target cursor` | `node tests/lib/install-targets.test.js`; `npm run harness:audit -- --format json` | Cursor adapters must preserve existing project rules and avoid silent overwrite. |
|
||||
| Gemini | Instruction-backed | Gemini project-local instructions; shared skills; rules; compatibility docs | No full ECC hook parity; ecosystem ports must document drift from upstream ECC | `./install.sh --profile minimal --target gemini` | `node tests/lib/install-targets.test.js` | Treat Gemini ports as ecosystem adapters until validated end to end inside Gemini CLI. |
|
||||
| Zed-adjacent workflows | Instruction-backed | shared skills; `AGENTS.md` style project instructions; verification loops | Zed agent surfaces vary; no first-party ECC installer is shipped today | Manual copy from shared ECC sources until adapter requirements settle | `npm run harness:audit -- --format json` | Do not claim native Zed support before a real adapter and verification path exist. |
|
||||
| dmux | Adapter-backed | session snapshots; tmux/worktree orchestration status; handoff exports | dmux is an orchestration runtime, not an install target for skills/rules | `node scripts/session-inspect.js --list-adapters`; dmux session target inspection | `node tests/lib/session-adapters.test.js` | Treat dmux events as session/runtime signals, not as a replacement for repo validation. |
|
||||
| Orca | Reference-only | worktree lifecycle; review state; notification; provider-identity design pressure | No ECC installer or direct adapter today | Use as a comparison target for worktree/session state requirements | `npm run observability:ready` | Do not import product-specific assumptions; convert lessons into ECC event fields. |
|
||||
| Superset | Reference-only | workspace presets; parallel-agent review loops; worktree isolation design pressure | No ECC installer or direct adapter today | Use as a comparison target for workspace preset taxonomy | `npm run observability:ready` | Keep ECC portable; do not require a desktop workspace to get basic value. |
|
||||
| Ghast | Reference-only | terminal-native pane grouping; cwd grouping; search; notifications | No ECC installer or direct adapter today | Use as a comparison target for terminal-first session grouping | `node scripts/session-inspect.js --list-adapters` | Preserve terminal ergonomics before adding visual UI assumptions. |
|
||||
| Terminal-only | Native | skills; rules; commands; scripts; harness audit; observability readiness; handoffs | No external UI, no automatic session control unless scripts are run explicitly | Clone repo; run commands directly; use minimal profile for project installs | `npm run harness:audit -- --format json`; `npm run observability:ready` | This is the fallback contract; every higher-level adapter should degrade to it. |
|
||||
<!-- harness-adapter-compliance:matrix-end -->
|
||||
|
||||
## Scorecard Onramp
|
||||
|
||||
Use this sequence before asking ECC to make a team or repo setup more
|
||||
autonomous:
|
||||
|
||||
```bash
|
||||
npm run harness:adapters -- --check
|
||||
npm run harness:audit -- --format json
|
||||
npm run observability:ready
|
||||
node scripts/session-inspect.js --list-adapters
|
||||
node scripts/loop-status.js --json --write-dir .ecc/loop-status
|
||||
```
|
||||
|
||||
Read the result as a setup scorecard, not a product badge:
|
||||
|
||||
- `harness:adapters -- --check` proves this public matrix still matches the
|
||||
adapter source data and required evidence fields.
|
||||
- `harness:audit` scores tool coverage, context efficiency, quality gates,
|
||||
memory persistence, eval coverage, security guardrails, and cost efficiency.
|
||||
- `observability:ready` proves the repo still exposes the local status,
|
||||
session, tool-activity, risk-ledger, and release-onramp signals.
|
||||
- `session-inspect --list-adapters` shows which session surfaces are actually
|
||||
inspectable in the current environment.
|
||||
- `loop-status --json` creates a machine-readable handoff/status payload for
|
||||
longer autonomous runs.
|
||||
|
||||
## Data-Backed Scorecard Contract
|
||||
|
||||
Each adapter record exposes:
|
||||
|
||||
- `id`
|
||||
- `state`
|
||||
- `supported_assets`
|
||||
- `unsupported_surfaces`
|
||||
- `install_or_onramp`
|
||||
- `verification_commands`
|
||||
- `risk_notes`
|
||||
- `last_verified_at`
|
||||
- `owner`
|
||||
- `source_docs`
|
||||
|
||||
The validator fails if a public adapter claim has no install path,
|
||||
verification command, risk note, owner, source doc, or verification date.
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Prefer small, additive adapters over harness-specific forks of the same
|
||||
workflow.
|
||||
- Do not call a harness native until the adapter has an install path and a
|
||||
verification command.
|
||||
- Keep Codex, Gemini, and Zed surfaces honest when enforcement is
|
||||
instruction-backed rather than runtime-backed.
|
||||
- Treat reference-only tools as design pressure until ECC has a direct adapter.
|
||||
- Keep the terminal-only path healthy; it is the portability floor.
|
||||
@@ -228,6 +228,10 @@ everything-claude-code/
|
||||
| |-- django-verification/ # Django 検証ループ(新規)
|
||||
| |-- python-patterns/ # Python イディオムとベストプラクティス(新規)
|
||||
| |-- python-testing/ # pytest を使った Python テスト(新規)
|
||||
| |-- quarkus-patterns/ # Quarkus アーキテクチャ、Camel、CDI、Panache パターン(新規)
|
||||
| |-- quarkus-security/ # Quarkus セキュリティ: JWT/OIDC、RBAC、バリデーション(新規)
|
||||
| |-- quarkus-tdd/ # Quarkus TDD: JUnit 5、Mockito、REST Assured(新規)
|
||||
| |-- quarkus-verification/ # Quarkus 検証: ビルド、テスト、ネイティブコンパイル(新規)
|
||||
| |-- springboot-patterns/ # Java Spring Boot パターン(新規)
|
||||
| |-- springboot-security/ # Spring Boot セキュリティ(新規)
|
||||
| |-- springboot-tdd/ # Spring Boot TDD(新規)
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
- `django-patterns/` - Django ベストプラクティス
|
||||
- `django-tdd/` - Django テスト駆動開発
|
||||
- `django-security/` - Django セキュリティ
|
||||
- `quarkus-patterns/` - Quarkus アーキテクチャ、Camel、CDI、Panache パターン
|
||||
- `quarkus-security/` - Quarkus セキュリティ: JWT/OIDC、RBAC、バリデーション
|
||||
- `quarkus-tdd/` - Quarkus テスト駆動開発
|
||||
- `quarkus-verification/` - Quarkus 検証ループ
|
||||
- `springboot-patterns/` - Spring Boot パターン
|
||||
- `springboot-tdd/` - Spring Boot テスト
|
||||
- `springboot-security/` - Spring Boot セキュリティ
|
||||
|
||||
@@ -65,7 +65,7 @@ mkdir -p $TARGET/skills $TARGET/rules
|
||||
|
||||
### 2a: スキルカテゴリの選択
|
||||
|
||||
27個のスキルが4つのカテゴリに分類されています。`multiSelect: true` で `AskUserQuestion` を使用します:
|
||||
31個のスキルが4つのカテゴリに分類されています。`multiSelect: true` で `AskUserQuestion` を使用します:
|
||||
|
||||
```
|
||||
Question: "どのスキルカテゴリをインストールしますか?"
|
||||
@@ -80,7 +80,7 @@ Options:
|
||||
|
||||
選択された各カテゴリについて、以下の完全なスキルリストを表示し、ユーザーに確認または特定のものの選択解除を依頼します。リストが4項目を超える場合、リストをテキストとして表示し、`AskUserQuestion` で「リストされたすべてをインストール」オプションと、ユーザーが特定の名前を貼り付けるための「その他」オプションを使用します。
|
||||
|
||||
**カテゴリ: Framework & Language(16スキル)**
|
||||
**カテゴリ: Framework & Language(20スキル)**
|
||||
|
||||
| スキル | 説明 |
|
||||
|-------|-------------|
|
||||
@@ -96,6 +96,10 @@ Options:
|
||||
| `java-coding-standards` | Spring Boot 用 Java コーディング標準: 命名、不変性、Optional、ストリーム |
|
||||
| `python-patterns` | Pythonic なイディオム、PEP 8、型ヒント、ベストプラクティス |
|
||||
| `python-testing` | pytest、TDD、フィクスチャ、モック、パラメータ化による Python テスト |
|
||||
| `quarkus-patterns` | Quarkus アーキテクチャ、Camel メッセージング、CDI サービス、Panache データアクセス |
|
||||
| `quarkus-security` | Quarkus セキュリティ: JWT/OIDC、RBAC、入力バリデーション、シークレット管理 |
|
||||
| `quarkus-tdd` | JUnit 5、Mockito、REST Assured、Camel テストによる Quarkus TDD |
|
||||
| `quarkus-verification` | Quarkus 検証: ビルド、静的解析、テスト、ネイティブコンパイル |
|
||||
| `springboot-patterns` | Spring Boot アーキテクチャ、REST API、レイヤードサービス、キャッシング、非同期 |
|
||||
| `springboot-security` | Spring Security: 認証/認可、検証、CSRF、シークレット、レート制限 |
|
||||
| `springboot-tdd` | JUnit 5、Mockito、MockMvc、Testcontainers による Spring Boot TDD |
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Legacy Artifact Inventory
|
||||
|
||||
This inventory keeps legacy and stale-work cleanup from becoming implicit. Each
|
||||
artifact should be classified as landed, milestone-tracked, salvage branch, or
|
||||
archive/no-action before release work treats the queue as clean.
|
||||
|
||||
## Classification States
|
||||
|
||||
| State | Meaning |
|
||||
| --- | --- |
|
||||
| Landed | Useful work has already been ported to current `main` and verified. |
|
||||
| Milestone-tracked | Useful work remains, but belongs to a named roadmap milestone. |
|
||||
| Salvage branch | Useful work should be ported through a fresh maintainer branch with attribution. |
|
||||
| Translator/manual review | Content may be useful, but cannot be safely imported automatically. |
|
||||
| Archive/no-action | Artifact is intentionally retained or skipped; no active port is planned. |
|
||||
|
||||
## Current Repository Scan
|
||||
|
||||
As of 2026-05-12, the tracked repo has no `_legacy-documents-*` directories.
|
||||
|
||||
Fresh check:
|
||||
|
||||
```sh
|
||||
find . -type d -name '_legacy-documents-*' -print
|
||||
```
|
||||
|
||||
Expected result: no output.
|
||||
|
||||
The only tracked legacy directory currently found by filename scan is
|
||||
`legacy-command-shims/`.
|
||||
|
||||
The umbrella ECC workspace also contains sibling legacy git repositories outside
|
||||
this tracked checkout. These are intentionally inventoried separately because
|
||||
they can contain raw operator context, local settings, private drafts, or
|
||||
untracked files that should not be copied into the public repo wholesale.
|
||||
|
||||
Fresh workspace-level check from the ECC umbrella directory:
|
||||
|
||||
```sh
|
||||
find .. -maxdepth 1 -type d -name '_legacy-documents-*' -print | sort
|
||||
```
|
||||
|
||||
Expected result:
|
||||
|
||||
```text
|
||||
../_legacy-documents-ecc-context-2026-04-30
|
||||
../_legacy-documents-ecc-everything-claude-code-2026-04-30
|
||||
```
|
||||
|
||||
## Inventory
|
||||
|
||||
| Artifact | State | Evidence | Action |
|
||||
| --- | --- | --- | --- |
|
||||
| `_legacy-documents-*` directories | Archive/no-action | No matching directories exist in the tracked checkout as of 2026-05-12. | Re-run the scan before release. If any appear, add each directory to this table before publishing. |
|
||||
| `legacy-command-shims/` | Archive/no-action | `legacy-command-shims/README.md` states these retired short-name shims are opt-in and no longer loaded by the default plugin command surface. | Keep as an explicit compatibility archive. Do not move these back into the default plugin surface without a migration decision. |
|
||||
| Closed-stale PR salvage ledger | Landed | `docs/stale-pr-salvage-ledger.md` records useful stale work recovered through maintainer PRs. | Continue using the ledger pattern for future stale closures. |
|
||||
| #1687 zh-CN localization tail | Translator/manual review | Large safe subsets landed in #1746-#1752; remaining pieces require translator/manual review per salvage ledger. | Do not blindly cherry-pick. Split by docs, commands, agents, and skills if a translator review lane opens. |
|
||||
|
||||
## Workspace-Level Legacy Repos
|
||||
|
||||
These sibling repositories live outside the tracked `everything-claude-code`
|
||||
checkout. They are source material for future salvage passes, not installable
|
||||
release assets.
|
||||
|
||||
| Artifact | State | Evidence | Action |
|
||||
| --- | --- | --- | --- |
|
||||
| `../_legacy-documents-ecc-everything-claude-code-2026-04-30` | Archive/no-action | Separate legacy checkout on `fix/configure-ecc-skill-copy-paths-1483` at `b78ddbd0`; useful configure-ecc and install-path concepts have been superseded by current install docs and tests. The checkout also has untracked localized project-guidelines examples and a Finder duplicate `skills/social-graph-ranker/SKILL 2.md`. | Do not import wholesale. If configure-ecc copy-root regressions reappear, use this branch only as source-attributed archaeology and port through a fresh maintainer branch. Leave Finder duplicates out of source control. |
|
||||
| `../_legacy-documents-ecc-context-2026-04-30` | Milestone-tracked | Archived `ECC-context` repo is four commits ahead of its origin and contains context, gameplan, knowledge, marketing, AgentShield, and ECC Tools planning material. It also contains local/private surfaces such as `.env` and local settings. | Keep as a sanitized extraction source for roadmap, launch, AgentShield, and ECC Tools work. Never copy raw context, secrets, personal paths, private settings, or unpublished drafts into this repo. Port only focused, public-safe content with attribution. |
|
||||
|
||||
## Workspace Legacy Import Rules
|
||||
|
||||
When mining workspace-level legacy repos:
|
||||
|
||||
1. Do not read, print, stage, or copy `.env` files, tokens, OAuth secrets,
|
||||
local settings, personal paths, or private operator context.
|
||||
2. Do not import raw marketing drafts, gameplans, or chat/context dumps.
|
||||
3. Extract only focused, public-safe ideas into current docs or code.
|
||||
4. Attribute the source legacy repo, branch, commit, or stale PR in the new PR.
|
||||
5. Validate the result with the same tests and release checks as native work.
|
||||
|
||||
## Legacy Command Shim Contents
|
||||
|
||||
The compatibility archive currently contains 12 retired command shims:
|
||||
|
||||
| Shim | Preferred current direction |
|
||||
| --- | --- |
|
||||
| `agent-sort.md` | Use maintained command or skill routing where available. |
|
||||
| `claw.md` | Use maintained `scripts/claw.js` / `npm run claw` surfaces. |
|
||||
| `context-budget.md` | Use maintained token/context budgeting skills. |
|
||||
| `devfleet.md` | Use maintained agent/harness orchestration docs and skills. |
|
||||
| `docs.md` | Use current documentation and release checklist workflows. |
|
||||
| `e2e.md` | Use maintained E2E testing skills and test scripts. |
|
||||
| `eval.md` | Use eval-harness and verification-loop skills. |
|
||||
| `orchestrate.md` | Use maintained orchestration status and worktree scripts. |
|
||||
| `prompt-optimize.md` | Use prompt-optimizer skill. |
|
||||
| `rules-distill.md` | Use current rules and skill extraction workflows. |
|
||||
| `tdd.md` | Use tdd-workflow and language-specific testing skills. |
|
||||
| `verify.md` | Use verification-loop and package-specific verification skills. |
|
||||
|
||||
## Release Rule
|
||||
|
||||
Before any GA or rc publication pass:
|
||||
|
||||
1. Re-run the `_legacy-documents-*` scan.
|
||||
2. Re-run the closed-stale salvage ledger check.
|
||||
3. Confirm every newly discovered legacy artifact is represented in this file.
|
||||
4. Port useful work through fresh maintainer PRs with source attribution.
|
||||
5. Leave archive/no-action artifacts out of default install and plugin loading.
|
||||
@@ -3,6 +3,7 @@
|
||||
## Repo
|
||||
|
||||
- verify local `main` is synced to `origin/main`
|
||||
- verify `docs/ECC-2.0-GA-ROADMAP.md` reflects the current Linear milestone plan
|
||||
- verify `docs/HERMES-SETUP.md` is present
|
||||
- verify `docs/architecture/cross-harness.md` is present
|
||||
- verify this release directory is committed
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
|
||||
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
|
||||
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
|
||||
- update release metadata in one dedicated release-version PR
|
||||
- run the root test suite
|
||||
- run `cd ecc2 && cargo test`
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# ECC v2.0.0-rc.1 Publication Readiness
|
||||
|
||||
This checklist is the release gate for public publication surfaces. Do not use
|
||||
it as evidence by itself. Fill the evidence fields with fresh command output or
|
||||
URLs from the exact commit being released.
|
||||
|
||||
## Release Identity Matrix
|
||||
|
||||
| Surface | Expected value | Source of truth | Fresh check | Evidence artifact | Owner | Status |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Product name | Everything Claude Code / ECC | `README.md`, `CHANGELOG.md`, release notes | `rg -n "Everything Claude Code" README.md CHANGELOG.md docs/releases/2.0.0-rc.1` | Pending | Release owner | Pending |
|
||||
| GitHub repo | `affaan-m/everything-claude-code` | Git remote and release URLs | `git remote get-url origin` | Pending | Release owner | Pending |
|
||||
| Git tag | `v2.0.0-rc.1` | GitHub releases | `gh release view v2.0.0-rc.1 --repo affaan-m/everything-claude-code` | Pending | Release owner | Pending |
|
||||
| npm package | `ecc-universal` | `package.json` | `node -p "require('./package.json').name"` | Pending | Package owner | Pending |
|
||||
| npm version | `2.0.0-rc.1` | `VERSION`, `package.json`, lockfiles | `node -p "require('./package.json').version"` | Pending | Package owner | Pending |
|
||||
| npm dist-tag | `next` for rc, `latest` only for GA | npm registry | `npm view ecc-universal dist-tags --json` | Pending | Package owner | Pending |
|
||||
| Claude plugin slug | `ecc` / `ecc@ecc` install path | `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` | `node tests/hooks/hooks.test.js` | Pending | Plugin owner | Pending |
|
||||
| Claude plugin manifest | `2.0.0-rc.1`, no unsupported `agents` or explicit `hooks` fields | `.claude-plugin/plugin.json`, `.claude-plugin/PLUGIN_SCHEMA_NOTES.md` | `claude plugin validate .claude-plugin/plugin.json` | Pending | Plugin owner | Pending |
|
||||
| Codex plugin manifest | `2.0.0-rc.1` with shared skill source | `.codex-plugin/plugin.json` | `node tests/docs/ecc2-release-surface.test.js` | Pending | Plugin owner | Pending |
|
||||
| OpenCode package | `ecc-universal` plugin module | `.opencode/package.json`, `.opencode/index.ts` | `npm run build:opencode` | Pending | Package owner | Pending |
|
||||
| Agent metadata | `2.0.0-rc.1` | `agent.yaml`, `.agents/plugins/marketplace.json` | `node tests/scripts/catalog.test.js` | Pending | Release owner | Pending |
|
||||
| Migration copy | rc.1 upgrade path, not GA claim | `release-notes.md`, `quickstart.md`, `HERMES-SETUP.md` | `npx markdownlint-cli docs/releases/2.0.0-rc.1/*.md` | Pending | Docs owner | Pending |
|
||||
|
||||
## Publication Gates
|
||||
|
||||
| Gate | Required evidence | Fresh check | Blocker field | Owner | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| GitHub release | Tag exists, release notes use final URLs, assets attached if needed | `gh release view v2.0.0-rc.1 --json tagName,url,isPrerelease` | `Blocker:` | Release owner | Pending |
|
||||
| npm package | `npm pack --dry-run` has expected files, version matches, rc goes to `next` | `npm pack --dry-run` and `npm publish --tag next --dry-run` where supported | `Blocker:` | Package owner | Pending |
|
||||
| Claude plugin | Manifest validates, marketplace JSON points to public repo, install docs match slug | `claude plugin validate .claude-plugin/plugin.json` | `Blocker:` | Plugin owner | Pending |
|
||||
| Codex plugin | Manifest version matches package and docs, hook limitations are explicit | `node tests/docs/ecc2-release-surface.test.js` | `Blocker:` | Plugin owner | Pending |
|
||||
| OpenCode package | Build output is regenerated from source and package metadata is current | `npm run build:opencode` | `Blocker:` | Package owner | Pending |
|
||||
| ECC Tools billing reference | Any billing claim links to verified Marketplace/App state | `gh api repos/ECC-Tools/ECC-Tools` plus app/marketplace URL check | `Blocker:` | ECC Tools owner | Pending |
|
||||
| Announcement copy | X, LinkedIn, GitHub release, and longform copy point to live URLs | `rg -n "TODO" docs/releases/2.0.0-rc.1` and repeat for `TBD` | `Blocker:` | Release owner | Pending |
|
||||
|
||||
## Required Command Evidence
|
||||
|
||||
Record the exact commit SHA and command output before any publication action:
|
||||
|
||||
| Evidence | Command | Required result | Recorded output |
|
||||
| --- | --- | --- | --- |
|
||||
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Pending |
|
||||
| Harness audit | `npm run harness:audit -- --format json` | 70/70 passing | Pending |
|
||||
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | Pending |
|
||||
| Observability readiness | `npm run observability:ready` | 14/14 passing | Pending |
|
||||
| Root suite | `node tests/run-all.js` | 0 failures | Pending |
|
||||
| Markdown lint | `npx markdownlint-cli '**/*.md' --ignore node_modules` | 0 failures | Pending |
|
||||
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | 0 failures | Pending |
|
||||
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | 0 failures | Pending |
|
||||
| Optional Rust surface | `cd ecc2 && cargo test` | 0 failures or explicit deferral | Pending |
|
||||
|
||||
## Do Not Publish If
|
||||
|
||||
- `main` has unreviewed release-surface changes after the evidence was recorded.
|
||||
- `npm view ecc-universal dist-tags --json` contradicts the intended rc/GA tag.
|
||||
- Claude plugin validation is unavailable and no manual install smoke test is
|
||||
recorded.
|
||||
- Release notes or announcement drafts still contain placeholder URLs,
|
||||
`TODO`, `TBD`, private workspace paths, or personal operator references.
|
||||
- Billing, Marketplace, or plugin-submission copy claims a live surface before
|
||||
the live URL exists.
|
||||
- Stale PR salvage work is mid-flight on the same branch.
|
||||
|
||||
## Announcement Order
|
||||
|
||||
1. Merge the release-version PR.
|
||||
2. Record the required command evidence from the release commit.
|
||||
3. Create or verify the GitHub prerelease.
|
||||
4. Publish npm with the rc dist-tag.
|
||||
5. Submit or update plugin marketplace surfaces.
|
||||
6. Update release notes with final live URLs.
|
||||
7. Publish GitHub release copy.
|
||||
8. Publish X, LinkedIn, and longform copy only after the public URLs work.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Stale PR Salvage Ledger
|
||||
|
||||
This ledger records useful work recovered from stale, conflicted, or closed PRs.
|
||||
The rule is simple: queue cleanup closes stale PRs, but it does not discard
|
||||
useful work. Maintainers should inspect the closed diff, port compatible pieces
|
||||
on fresh branches, and credit the source PR.
|
||||
|
||||
## Classification States
|
||||
|
||||
| State | Meaning |
|
||||
| --- | --- |
|
||||
| Salvaged | Useful work was ported to current `main` through a maintainer PR. |
|
||||
| Already present | Current `main` already contained the useful work before salvage. |
|
||||
| Superseded | Current `main` solved the same problem differently. |
|
||||
| Skipped | The PR was accidental, too broad, unsafe, or too low-signal to port. |
|
||||
| Translator/manual review | Content may be useful, but needs human language/domain review before import. |
|
||||
|
||||
## Salvaged Into Current Main
|
||||
|
||||
| Source PR | Original contribution | Salvage result |
|
||||
| --- | --- | --- |
|
||||
| #1309 | Trading/community project material | Salvaged in #1761 as a neutral community-project README listing. |
|
||||
| #1322 | Vietnamese README translation | Salvaged in #1764 as `docs/vi-VN/README.md` plus selector updates. |
|
||||
| #1326 | Angular developer skill and rules | Salvaged in #1763 with current skill, rules, install wiring, and catalog updates. |
|
||||
| #1328 | Continuous-learning Windows UTF-8 stdout fix | Salvaged in #1761. |
|
||||
| #1329 | Plugin install detection hardening | Salvaged in #1761 through current harness audit detection support. |
|
||||
| #1334 | Windows desktop E2E skill | Salvaged in #1762 with install, package, and catalog wiring. |
|
||||
| #1352 | Qwen install target | Salvaged in #1738 through the current Qwen install target. |
|
||||
| #1413 | Network and homelab skills/agents | Salvaged through #1729, #1731, #1745, and #1778. |
|
||||
| #1429 | JoyCode install target | Salvaged in #1737 through the current JoyCode install target. |
|
||||
| #1467 | Scientific skills and OpenCode discovery work | Useful USPTO and gget pieces salvaged in #1740; stale generated claims were not copied. |
|
||||
| #1493 | SessionStart context scoping | Salvaged in #1774 with current hook semantics and tests. |
|
||||
| #1498 | PRD planning flow | Salvaged in #1777. |
|
||||
| #1504 | Statusline/context monitor hooks | Salvaged in #1776 with current hook manifest structure and tests. |
|
||||
| #1528/#1529/#1547 | Astraflow and UModelVerse provider support | Salvaged in #1775 with current provider wiring and defensive tool-call parsing. |
|
||||
| #1558 | `agentic-os` skill | Salvaged in #1772. |
|
||||
| #1559 | `error-handling` skill | Salvaged in #1772. |
|
||||
| #1566 | Agent architecture audit skill | Salvaged in #1772. |
|
||||
| #1578 | OpenCode file-probe hardening | Salvaged in #1773. |
|
||||
| #1674 | Production audit skill | Salvaged in #1732 after supply-chain/privacy review and rewrite. |
|
||||
| #1687 | zh-CN localization sync | Large safe subsets salvaged in #1746-#1752; remaining pieces require translator/manual review. |
|
||||
| #1694 | Portfolio curation | Useful focused curation updates salvaged in #1723 and #1724. |
|
||||
| #1695 | Russian README translation | Ported in #1722. |
|
||||
| #1697 | Saved LLM selector config | Salvaged as part of provider config/tool schema work in #1720. |
|
||||
| #1699 | Windows post-edit-format path guard | Ported in #1719. |
|
||||
| #1700 | Provider tool serialization | Ported in #1720. |
|
||||
| #1705/#1780 | Production UI motion system | Salvaged in #1772, #1781, and #1782 with examples fixed before merge. |
|
||||
| #1713 | Swift language support | Ported in #1721. |
|
||||
| #1715 | CI personal-path validator hardening | Ported through CI validator hardening in #1717. |
|
||||
| #1727 | MySQL patterns skill | Salvaged in #1733. |
|
||||
| #1757 | Machine-learning engineering workflow | Salvaged in #1758 and tuned in #1759. |
|
||||
|
||||
## Already Present Or Superseded
|
||||
|
||||
| Source PR | Disposition |
|
||||
| --- | --- |
|
||||
| #1306 | Hook bug workarounds already exist on `main` as `docs/hook-bug-workarounds.md`. |
|
||||
| #1318 | Gemini agent adaptation utility was already present on current `main`. |
|
||||
| #1323 | Hook config update was already present on current `main`. |
|
||||
| #1337 | Catalog count update was superseded by current catalog-count sync. |
|
||||
| #1682/#1701 | Strategic compact hook-path fixes were merged directly or superseded by current docs fixes. |
|
||||
| JARVIS #4/#5/#6 | Stale failing dependency-only PRs; future dependency state should be regenerated by Dependabot. |
|
||||
|
||||
## Skipped
|
||||
|
||||
| Source PR | Reason |
|
||||
| --- | --- |
|
||||
| #1308 | Stale zh-CN sync would rewind or delete too much current tree state; concrete selector-link fix was already present. |
|
||||
| #1320 | Package-manager removal conflicts with the current npm/pnpm/yarn/bun CI policy. |
|
||||
| #1341 | Very large low-signal generated change with no safe focused salvage unit. |
|
||||
| #1416/#1465 | Accidental fork-sync PRs with no focused contribution. |
|
||||
| #1475 | One-line Gemini CLI bridge idea was too stale and underspecified to port safely. |
|
||||
|
||||
## Remaining Manual-Review Backlog
|
||||
|
||||
Only the #1687 localization tail remains plausibly useful but unsafe to
|
||||
auto-port.
|
||||
|
||||
Handling rule:
|
||||
|
||||
1. Keep #1687 in translator/manual review.
|
||||
2. Split any future work by surface: agents, commands, top-level docs, release
|
||||
and count surfaces, then skills.
|
||||
3. Do not import stale top-level docs that carry old version or catalog-count
|
||||
facts.
|
||||
4. Do not reopen old PRs unless the original author returns with a current
|
||||
rebase; maintainer-side salvage should happen on fresh branches with
|
||||
attribution.
|
||||
|
||||
## Future Cleanup Rule
|
||||
|
||||
For every stale/conflicted PR cleanup batch:
|
||||
|
||||
1. Close or comment on the PR based on the queue policy.
|
||||
2. Add the source PR to this ledger or a dated successor ledger.
|
||||
3. Classify it as salvaged, already present, superseded, skipped, or
|
||||
translator/manual review.
|
||||
4. If useful, port a small compatible slice on a fresh maintainer branch.
|
||||
5. Credit the source PR and author in the maintainer PR body.
|
||||
@@ -150,4 +150,6 @@ Remaining errors: 1
|
||||
|
||||
Son: `Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||
|
||||
Detaylı Java ve Spring Boot kalıpları için, `skill: springboot-patterns`'a bakın.
|
||||
Detaylı Java kalıpları ve örnekler için:
|
||||
- **[SPRING]**: `skill: springboot-patterns`'a bakın
|
||||
- **[QUARKUS]**: `skill: quarkus-patterns`'a bakın
|
||||
|
||||
@@ -89,4 +89,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
||||
- **Uyarı**: Sadece MEDIUM sorunlar
|
||||
- **Bloke Et**: CRITICAL veya HIGH sorunlar bulundu
|
||||
|
||||
Detaylı Spring Boot kalıpları ve örnekleri için, `skill: springboot-patterns`'a bakın.
|
||||
Detaylı kalıplar ve örnekler için:
|
||||
- **[SPRING]**: `skill: springboot-patterns`'a bakın
|
||||
- **[QUARKUS]**: `skill: quarkus-patterns`'a bakın
|
||||
|
||||
@@ -0,0 +1,778 @@
|
||||
---
|
||||
name: quarkus-patterns
|
||||
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Quarkus Geliştirme Desenleri
|
||||
|
||||
Apache Camel ile bulut-native, event-driven servisler için Quarkus 3.x mimari ve API desenleri.
|
||||
|
||||
## When to Use
|
||||
|
||||
- JAX-RS veya RESTEasy Reactive ile REST API'leri oluşturma
|
||||
- Resource → service → repository katmanlarını yapılandırma
|
||||
- Apache Camel ve RabbitMQ ile event-driven desenler uygulama
|
||||
- Hibernate Panache, caching veya reaktif akışları yapılandırma
|
||||
- Validation, exception mapping veya sayfalama ekleme
|
||||
- Dev/staging/production ortamları için profiller kurma (YAML yapılandırma)
|
||||
- LogContext ve Logback/Logstash encoder ile özel loglama
|
||||
- Async işlemler için CompletableFuture ile çalışma
|
||||
- Koşullu akış işleme uygulama
|
||||
- GraalVM native derleme ile çalışma
|
||||
|
||||
## How It Works
|
||||
|
||||
Quarkus servislerinde Resource -> service -> repository akışını CDI scope'ları,
|
||||
`@Transactional` sınırları, Panache/Hibernate veri erişimi ve Camel/RabbitMQ
|
||||
entegrasyonlarıyla birlikte uygulayın. Aşağıdaki örnekler event üretimi,
|
||||
dosya işleme, özel logging context ve async yayınlama için kopyalanabilir
|
||||
başlangıç noktaları sağlar.
|
||||
|
||||
## Examples
|
||||
|
||||
### Birden Fazla Bağımlılıklı Service Katmanı (Lombok)
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class As2ProcessingService {
|
||||
|
||||
private final InvoiceFlowValidator invoiceFlowValidator;
|
||||
private final EventService eventService;
|
||||
private final DocumentJobService documentJobService;
|
||||
private final BusinessRulesPublisher businessRulesPublisher;
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
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,
|
||||
EInvoiceSyntaxFormat.UBL, logContext);
|
||||
|
||||
FlowProfile flowProfile = isChorusFlow ?
|
||||
FlowProfile.EXTENDED_CTC_FR :
|
||||
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<StoredDocumentInfo> 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,
|
||||
flowProfile, invoiceValidationResult.getDocumentHash());
|
||||
|
||||
// Async Camel yayınlama
|
||||
businessRulesPublisher.publishAsync(payload);
|
||||
this.eventService.createSuccessEvent(payload, "BUSINESS_RULES_MESSAGE_SENT");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Temel Desenler:**
|
||||
- Constructor injection için Lombok üzerinden `@RequiredArgsConstructor`
|
||||
- Logback loglama için `@Slf4j`
|
||||
- try-with-resources ile kapsamlı LogContext
|
||||
- Runtime parametrelerine dayalı koşullu akış mantığı
|
||||
- Async işlemler için `.join()` ile CompletableFuture
|
||||
- Başarı/hata senaryoları için event takibi
|
||||
- Async Camel mesaj yayınlama
|
||||
|
||||
## Özel Loglama Bağlamı Deseni (Logback)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class ProcessingService {
|
||||
|
||||
public void processDocument(Document doc) {
|
||||
LogContext logContext = CustomLog.getCurrentContext();
|
||||
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
||||
// Tüm log ifadelerine bağlam ekle
|
||||
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);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Logback Yapılandırması (logback.xml):**
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||
<includeContext>true</includeContext>
|
||||
<includeMdc>true</includeMdc>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.example" level="INFO"/>
|
||||
<root level="WARN">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
### Event Service Deseni
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
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();
|
||||
event.setType(eventType);
|
||||
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()) {
|
||||
throw new IllegalArgumentException("Error message cannot be blank");
|
||||
}
|
||||
Event event = new Event();
|
||||
event.setType(eventType);
|
||||
event.setStatus(EventStatus.ERROR);
|
||||
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);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to serialize event payload", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Mesaj Yayınlama (RabbitMQ)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@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",
|
||||
payload
|
||||
);
|
||||
log.info("Message published to business rules queue: {}", payload.getDocumentId());
|
||||
}
|
||||
|
||||
public void publishSync(BusinessRulesPayload payload) {
|
||||
producerTemplate.sendBody(
|
||||
"direct:business-rules-publisher",
|
||||
payload
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Camel Route Yapılandırması:**
|
||||
|
||||
```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",
|
||||
businessRulesQueue, rabbitHost, rabbitPort);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Direct Route'ları (Bellek İçi)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class DocumentProcessingRoute extends RouteBuilder {
|
||||
|
||||
@Override
|
||||
public void configure() {
|
||||
// Hata yönetimi
|
||||
onException(ValidationException.class)
|
||||
.handled(true)
|
||||
.to("direct:validation-error-handler")
|
||||
.log("Validation error: ${exception.message}");
|
||||
|
||||
// Ana işleme route'u
|
||||
from("direct:process-document")
|
||||
.routeId("document-processing")
|
||||
.log("Processing document: ${header.documentId}")
|
||||
.bean(DocumentValidator.class, "validate")
|
||||
.bean(DocumentTransformer.class, "transform")
|
||||
.choice()
|
||||
.when(header("documentType").isEqualTo("INVOICE"))
|
||||
.to("direct:process-invoice")
|
||||
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
|
||||
.to("direct:process-credit-note")
|
||||
.otherwise()
|
||||
.to("direct:process-generic")
|
||||
.end();
|
||||
|
||||
from("direct:validation-error-handler")
|
||||
.bean(EventService.class, "createErrorEvent")
|
||||
.log("Validation error handled");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Dosya İşleme
|
||||
|
||||
```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 +
|
||||
"&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");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Camel Bean Çağrısı
|
||||
|
||||
```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")
|
||||
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## REST API Yapısı
|
||||
|
||||
```java
|
||||
@Path("/api/documents")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentResource {
|
||||
private final DocumentService documentService;
|
||||
|
||||
@GET
|
||||
public Response list(
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("size") @DefaultValue("20") int size) {
|
||||
List<Document> documents = documentService.list(page, size);
|
||||
return Response.ok(documents).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
|
||||
Document document = documentService.create(request);
|
||||
URI location = uriInfo.getAbsolutePathBuilder()
|
||||
.path(String.valueOf(document.id))
|
||||
.build();
|
||||
return Response.created(location).entity(DocumentResponse.from(document)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public Response getById(@PathParam("id") Long id) {
|
||||
return documentService.findById(id)
|
||||
.map(DocumentResponse::from)
|
||||
.map(Response::ok)
|
||||
.orElse(Response.status(Response.Status.NOT_FOUND))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Deseni (Panache Repository)
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class DocumentRepository implements PanacheRepository<Document> {
|
||||
|
||||
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
||||
return find("status = ?1 order by createdAt desc", status)
|
||||
.page(page, size)
|
||||
.list();
|
||||
}
|
||||
|
||||
public Optional<Document> 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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Transaction'lı Service Katmanı
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentService {
|
||||
private final DocumentRepository repo;
|
||||
private final EventService eventService;
|
||||
|
||||
@Transactional
|
||||
public Document create(CreateDocumentRequest request) {
|
||||
Document document = new Document();
|
||||
document.setReferenceNumber(request.referenceNumber());
|
||||
document.setDescription(request.description());
|
||||
document.setStatus(DocumentStatus.PENDING);
|
||||
document.setCreatedAt(Instant.now());
|
||||
|
||||
repo.persist(document);
|
||||
|
||||
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
public Optional<Document> findById(Long id) {
|
||||
return repo.findByIdOptional(id);
|
||||
}
|
||||
|
||||
public List<Document> list(int page, int size) {
|
||||
return repo.findAll()
|
||||
.page(page, size)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DTO'lar ve Validation
|
||||
|
||||
```java
|
||||
public record CreateDocumentRequest(
|
||||
@NotBlank @Size(max = 200) String referenceNumber,
|
||||
@NotBlank @Size(max = 2000) String description,
|
||||
@NotNull @FutureOrPresent Instant validUntil,
|
||||
@NotEmpty List<@NotBlank String> categories) {}
|
||||
|
||||
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
||||
public static DocumentResponse from(Document document) {
|
||||
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
||||
document.getStatus());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exception Eşleme
|
||||
|
||||
```java
|
||||
@Provider
|
||||
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
|
||||
@Override
|
||||
public Response toResponse(ConstraintViolationException exception) {
|
||||
String message = exception.getConstraintViolations().stream()
|
||||
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "validation_error", "message", message))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Provider
|
||||
@Slf4j
|
||||
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
||||
|
||||
@Override
|
||||
public Response toResponse(Exception exception) {
|
||||
log.error("Unhandled exception", exception);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "internal_error", "message", "An unexpected error occurred"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CompletableFuture Async İşlemleri
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class FileStorageService {
|
||||
private final S3Client s3Client;
|
||||
private final ExecutorService executorService;
|
||||
|
||||
@ConfigProperty(name = "storage.bucket-name") String bucketName;
|
||||
|
||||
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
||||
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);
|
||||
throw new StorageException("Upload failed", e);
|
||||
}
|
||||
}, executorService);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentCacheService {
|
||||
private final DocumentRepository repo;
|
||||
|
||||
@CacheResult(cacheName = "document-cache")
|
||||
public Optional<Document> getById(@CacheKey Long id) {
|
||||
return repo.findByIdOptional(id);
|
||||
}
|
||||
|
||||
@CacheInvalidate(cacheName = "document-cache")
|
||||
public void evict(@CacheKey Long id) {}
|
||||
|
||||
@CacheInvalidateAll(cacheName = "document-cache")
|
||||
public void evictAll() {}
|
||||
}
|
||||
```
|
||||
|
||||
## YAML Yapılandırması
|
||||
|
||||
```yaml
|
||||
# application.yml (uygulama yapılandırması)
|
||||
"%dev":
|
||||
quarkus:
|
||||
datasource:
|
||||
jdbc:
|
||||
url: jdbc:postgresql://localhost:5432/dev_db
|
||||
username: dev_user
|
||||
password: dev_pass
|
||||
hibernate-orm:
|
||||
database:
|
||||
generation: drop-and-create
|
||||
|
||||
rabbitmq:
|
||||
host: localhost
|
||||
port: 5672
|
||||
username: guest
|
||||
password: guest
|
||||
|
||||
"%test":
|
||||
quarkus:
|
||||
datasource:
|
||||
jdbc:
|
||||
url: jdbc:h2:mem:test
|
||||
hibernate-orm:
|
||||
database:
|
||||
generation: drop-and-create
|
||||
|
||||
"%prod":
|
||||
quarkus:
|
||||
datasource:
|
||||
jdbc:
|
||||
url: ${DATABASE_URL}
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASSWORD}
|
||||
hibernate-orm:
|
||||
database:
|
||||
generation: validate
|
||||
|
||||
rabbitmq:
|
||||
host: ${RABBITMQ_HOST}
|
||||
port: ${RABBITMQ_PORT}
|
||||
username: ${RABBITMQ_USER}
|
||||
password: ${RABBITMQ_PASSWORD}
|
||||
|
||||
# Camel yapılandırması
|
||||
camel:
|
||||
rabbitmq:
|
||||
queue:
|
||||
business-rules: business-rules-queue
|
||||
invoice-processing: invoice-processing-queue
|
||||
```
|
||||
|
||||
## Sağlık Kontrolleri
|
||||
|
||||
```java
|
||||
@Readiness
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class DatabaseHealthCheck implements HealthCheck {
|
||||
private final AgroalDataSource dataSource;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
try (Connection conn = dataSource.getConnection()) {
|
||||
boolean valid = conn.isValid(2);
|
||||
return HealthCheckResponse.named("Database connection")
|
||||
.status(valid)
|
||||
.build();
|
||||
} catch (SQLException e) {
|
||||
return HealthCheckResponse.down("Database connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Liveness
|
||||
@ApplicationScoped
|
||||
public class CamelHealthCheck implements HealthCheck {
|
||||
@Inject
|
||||
CamelContext camelContext;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
boolean isStarted = camelContext.getStatus().isStarted();
|
||||
return HealthCheckResponse.named("Camel Context")
|
||||
.status(isStarted)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bağımlılıklar (Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<quarkus.platform.version>3.27.0</quarkus.platform.version>
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<assertj-core.version>3.24.2</assertj-core.version>
|
||||
<jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-bom</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-camel-bom</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Quarkus Çekirdek -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-config-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Camel Uzantıları -->
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-direct</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-bean</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Loglama -->
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.logging.logback</groupId>
|
||||
<artifactId>quarkus-logging-logback</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
### Mimari
|
||||
- Constructor injection için Lombok üzerinden `@RequiredArgsConstructor` kullanın
|
||||
- Service katmanını ince tutun; karmaşık mantığı uzmanlaşmış sınıflara devredin
|
||||
- Mesaj yönlendirme ve entegrasyon desenleri için Camel route'larını kullanın
|
||||
- Veri erişimi için Panache Repository desenini tercih edin
|
||||
|
||||
### Event-Driven
|
||||
- EventService ile işlemleri her zaman takip edin (başarı/hata eventleri)
|
||||
- Bellek içi yönlendirme için Camel `direct:` endpoint'leri kullanın
|
||||
- RabbitMQ entegrasyonu için `spring-rabbitmq` bileşenini kullanın
|
||||
- `ProducerTemplate.asyncSendBody()` ile async yayınlama uygulayın
|
||||
|
||||
### Loglama
|
||||
- Yapılandırılmış loglama için Logstash encoder ile Logback kullanın
|
||||
- LogContext'i `SafeAutoCloseable` ile servis çağrıları boyunca yayın
|
||||
- İstek takibi için LogContext'e bağlamsal bilgi ekleyin
|
||||
- Manuel logger oluşturma yerine `@Slf4j` kullanın
|
||||
|
||||
### Async İşlemler
|
||||
- Bloklamayan I/O işlemleri için CompletableFuture kullanın
|
||||
- Tamamlanmayı beklemek gerektiğinde `.join()` çağırın
|
||||
- CompletableFuture'dan gelen exception'ları düzgün şekilde ele alın
|
||||
- Takip için async işlemlere LogContext geçirin
|
||||
|
||||
### Yapılandırma
|
||||
- YAML yapılandırmasını kullanın (`quarkus-config-yaml`)
|
||||
- Dev/test/prod ortamları için profil-duyarlı yapılandırma
|
||||
- Hassas yapılandırmayı ortam değişkenlerine dışsallaştırın
|
||||
- Tip-güvenli yapılandırma injection için `@ConfigProperty` kullanın
|
||||
|
||||
### Validation
|
||||
- Resource katmanında `@Valid` ile doğrulayın
|
||||
- DTO'larda Bean Validation annotasyonları kullanın
|
||||
- Exception'ları `@Provider` ile uygun HTTP yanıtlarına eşleyin
|
||||
|
||||
### Transaction'lar
|
||||
- Veri değiştiren service metodlarında `@Transactional` kullanın
|
||||
- Transaction'ları kısa ve odaklı tutun
|
||||
- Transaction'lar içinden async işlem çağırmaktan kaçının
|
||||
|
||||
### Test
|
||||
- Route testi için `camel-quarkus-junit5` kullanın
|
||||
- Assertion'lar için AssertJ kullanın
|
||||
- Tüm harici bağımlılıkları mock'layın
|
||||
- Koşullu akış mantığını kapsamlı biçimde test edin
|
||||
|
||||
### Quarkus'a Özgü
|
||||
- En son LTS sürümünde kalın (3.x)
|
||||
- Hot reload için Quarkus dev modunu kullanın
|
||||
- Production hazırlığı için sağlık kontrolleri ekleyin
|
||||
- Native derleme uyumluluğunu periyodik olarak test edin
|
||||
@@ -0,0 +1,474 @@
|
||||
---
|
||||
name: quarkus-security
|
||||
description: Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Quarkus Güvenlik İncelemesi
|
||||
|
||||
Kimlik doğrulama, yetkilendirme ve girdi doğrulama ile Quarkus uygulamalarını güvenli hale getirmek için en iyi uygulamalar.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Kimlik doğrulama ekleme (JWT, OIDC, Basic Auth)
|
||||
- `@RolesAllowed` veya SecurityIdentity ile yetkilendirme uygulama
|
||||
- Kullanıcı girişini doğrulama (Bean Validation, özel doğrulayıcılar)
|
||||
- CORS veya güvenlik başlıklarını yapılandırma
|
||||
- Gizli bilgileri yönetme (Vault, ortam değişkenleri, config kaynakları)
|
||||
- Rate limiting veya brute-force koruması ekleme
|
||||
- Bağımlılıkları CVE için tarama
|
||||
- MicroProfile JWT veya SmallRye JWT ile çalışma
|
||||
|
||||
## How It Works
|
||||
|
||||
Quarkus güvenliğini katmanlı uygulayın: JWT/OIDC veya Basic Auth ile kimliği
|
||||
doğrulayın, `SecurityIdentity` ve `@RolesAllowed` ile yetki kararlarını
|
||||
merkezileştirin, Bean Validation ile girdileri sınırlandırın, CORS ve güvenlik
|
||||
başlıklarını açıkça yapılandırın, gizli bilgileri Vault veya ortam değişkenleri
|
||||
üzerinden yönetin.
|
||||
|
||||
## Examples
|
||||
|
||||
### Kimlik Doğrulama
|
||||
|
||||
### JWT Kimlik Doğrulama
|
||||
|
||||
```java
|
||||
// JWT ile korunan resource
|
||||
@Path("/api/protected")
|
||||
@Authenticated
|
||||
public class ProtectedResource {
|
||||
|
||||
@Inject
|
||||
JsonWebToken jwt;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@GET
|
||||
public Response getData() {
|
||||
String username = jwt.getName();
|
||||
Set<String> roles = jwt.getGroups();
|
||||
return Response.ok(Map.of(
|
||||
"username", username,
|
||||
"roles", roles,
|
||||
"principal", securityIdentity.getPrincipal().getName()
|
||||
)).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Yapılandırma (application.properties):
|
||||
```properties
|
||||
mp.jwt.verify.publickey.location=publicKey.pem
|
||||
mp.jwt.verify.issuer=https://auth.example.com
|
||||
|
||||
# OIDC
|
||||
quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm
|
||||
quarkus.oidc.client-id=backend-service
|
||||
quarkus.oidc.credentials.secret=${OIDC_SECRET}
|
||||
```
|
||||
|
||||
### Özel Kimlik Doğrulama Filtresi
|
||||
|
||||
```java
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean validateToken(String token) {
|
||||
// Token doğrulama mantığı
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Yetkilendirme
|
||||
|
||||
### Rol Tabanlı Erişim Kontrolü
|
||||
|
||||
```java
|
||||
@Path("/api/admin")
|
||||
@RolesAllowed("ADMIN")
|
||||
public class AdminResource {
|
||||
|
||||
@GET
|
||||
@Path("/users")
|
||||
public List<UserDto> listUsers() {
|
||||
return userService.findAll();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/users/{id}")
|
||||
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
|
||||
public Response deleteUser(@PathParam("id") Long id) {
|
||||
userService.delete(id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/api/users")
|
||||
public class UserResource {
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@RolesAllowed("USER")
|
||||
public Response getUser(@PathParam("id") Long id) {
|
||||
// Sahipliği kontrol et
|
||||
if (!securityIdentity.hasRole("ADMIN") &&
|
||||
!isOwner(id, securityIdentity.getPrincipal().getName())) {
|
||||
return Response.status(Response.Status.FORBIDDEN).build();
|
||||
}
|
||||
return Response.ok(userService.findById(id)).build();
|
||||
}
|
||||
|
||||
private boolean isOwner(Long userId, String username) {
|
||||
return userService.isOwner(userId, username);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Programatik Güvenlik
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class SecurityService {
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
public boolean canAccessResource(Long resourceId) {
|
||||
if (securityIdentity.isAnonymous()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (securityIdentity.hasRole("ADMIN")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String userId = securityIdentity.getPrincipal().getName();
|
||||
return resourceRepository.isOwner(resourceId, userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Girdi Doğrulama
|
||||
|
||||
### Bean Validation
|
||||
|
||||
```java
|
||||
// KÖTÜ: Validation yok
|
||||
@POST
|
||||
public Response createUser(UserDto dto) {
|
||||
return Response.ok(userService.create(dto)).build();
|
||||
}
|
||||
|
||||
// İYİ: Doğrulanmış DTO
|
||||
public record CreateUserDto(
|
||||
@NotBlank @Size(max = 100) String name,
|
||||
@NotBlank @Email String email,
|
||||
@NotNull @Min(18) @Max(150) Integer age,
|
||||
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
|
||||
) {}
|
||||
|
||||
@POST
|
||||
@Path("/users")
|
||||
public Response createUser(@Valid CreateUserDto dto) {
|
||||
User user = userService.create(dto);
|
||||
return Response.status(Response.Status.CREATED).entity(user).build();
|
||||
}
|
||||
```
|
||||
|
||||
### Özel Doğrulayıcılar
|
||||
|
||||
```java
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = UsernameValidator.class)
|
||||
public @interface ValidUsername {
|
||||
String message() default "Invalid username format";
|
||||
Class<?>[] groups() default {};
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
}
|
||||
|
||||
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null) return false;
|
||||
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanım
|
||||
public record CreateUserDto(
|
||||
@ValidUsername String username,
|
||||
@NotBlank @Email String email
|
||||
) {}
|
||||
```
|
||||
|
||||
## SQL Injection Önleme
|
||||
|
||||
### Panache Active Record (Varsayılan Olarak Güvenli)
|
||||
|
||||
```java
|
||||
// İYİ: Panache ile parametreli sorgular
|
||||
List<User> users = User.list("email = ?1 and active = ?2", email, true);
|
||||
|
||||
Optional<User> user = User.find("username", username).firstResultOptional();
|
||||
|
||||
// İYİ: İsimlendirilmiş parametreler
|
||||
List<User> users = User.list("email = :email and age > :minAge",
|
||||
Parameters.with("email", email).and("minAge", 18));
|
||||
```
|
||||
|
||||
### Native Sorgular (Parametre Kullanın)
|
||||
|
||||
```java
|
||||
// KÖTÜ: String birleştirme
|
||||
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
||||
|
||||
// İYİ: Parametreli native sorgu
|
||||
@Entity
|
||||
public class User extends PanacheEntity {
|
||||
public static List<User> findByEmailNative(String email) {
|
||||
return getEntityManager()
|
||||
.createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)
|
||||
.setParameter("email", email)
|
||||
.getResultList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Parola Hash'leme
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class PasswordService {
|
||||
|
||||
public String hash(String plainPassword) {
|
||||
return BcryptUtil.bcryptHash(plainPassword);
|
||||
}
|
||||
|
||||
public boolean verify(String plainPassword, String hashedPassword) {
|
||||
return BcryptUtil.matches(plainPassword, hashedPassword);
|
||||
}
|
||||
}
|
||||
|
||||
// Servis içinde
|
||||
@ApplicationScoped
|
||||
public class UserService {
|
||||
@Inject
|
||||
PasswordService passwordService;
|
||||
|
||||
@Transactional
|
||||
public User register(CreateUserDto dto) {
|
||||
String hashedPassword = passwordService.hash(dto.password());
|
||||
User user = new User();
|
||||
user.email = dto.email();
|
||||
user.password = hashedPassword;
|
||||
user.persist();
|
||||
return user;
|
||||
}
|
||||
|
||||
public boolean authenticate(String email, String password) {
|
||||
return User.find("email", email)
|
||||
.firstResultOptional()
|
||||
.map(u -> passwordService.verify(password, u.password))
|
||||
.orElse(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CORS Yapılandırması
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=https://app.example.com,https://admin.example.com
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE
|
||||
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
|
||||
quarkus.http.cors.exposed-headers=Content-Disposition
|
||||
quarkus.http.cors.access-control-max-age=24H
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
```
|
||||
|
||||
## Gizli Bilgi Yönetimi
|
||||
|
||||
```properties
|
||||
# application.properties - BURADA GİZLİ BİLGİ YOK
|
||||
|
||||
# Ortam değişkenlerini kullanın
|
||||
quarkus.datasource.username=${DB_USER}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
||||
|
||||
# Or use Vault
|
||||
quarkus.vault.url=https://vault.example.com
|
||||
quarkus.vault.authentication.kubernetes.role=my-role
|
||||
```
|
||||
|
||||
### HashiCorp Vault Entegrasyonu
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class SecretService {
|
||||
|
||||
@ConfigProperty(name = "api-key")
|
||||
String apiKey; // Vault'tan alınır
|
||||
|
||||
public String getSecret(String key) {
|
||||
return ConfigProvider.getConfig().getValue(key, String.class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting (Hız Sınırlama)
|
||||
|
||||
**Güvenlik Notu**: `X-Forwarded-For` doğrudan kullanmayın — istemciler bunu taklit edebilir.
|
||||
Servlet request'ten gerçek uzak adresi veya kimliği doğrulanmış bir kimlik (API anahtarı, JWT subject) kullanın.
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class RateLimitFilter implements ContainerRequestFilter {
|
||||
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
HttpServletRequest servletRequest;
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
String clientId = getClientIdentifier();
|
||||
RateLimiter limiter = limiters.computeIfAbsent(clientId,
|
||||
k -> RateLimiter.create(100.0)); // Saniyede 100 istek
|
||||
|
||||
if (!limiter.tryAcquire()) {
|
||||
requestContext.abortWith(
|
||||
Response.status(429)
|
||||
.entity(Map.of("error", "Too many requests"))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientIdentifier() {
|
||||
// Konteyner tarafından sağlanan uzak adresi kullanın (X-Forwarded-For değil).
|
||||
// Güvenilir proxy arkasındaysanız quarkus.http.proxy.proxy-address-forwarding=true ayarlayın.
|
||||
return servletRequest.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Güvenlik Başlıkları
|
||||
|
||||
```java
|
||||
@Provider
|
||||
public class SecurityHeadersFilter implements ContainerResponseFilter {
|
||||
|
||||
@Override
|
||||
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
|
||||
MultivaluedMap<String, Object> 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",
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Denetim Loglama
|
||||
|
||||
```java
|
||||
@ApplicationScoped
|
||||
public class AuditService {
|
||||
private static final Logger LOG = Logger.getLogger(AuditService.class);
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
public void logAccess(String resource, String action) {
|
||||
String user = securityIdentity.isAnonymous()
|
||||
? "anonymous"
|
||||
: securityIdentity.getPrincipal().getName();
|
||||
|
||||
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
|
||||
user, action, resource, Instant.now());
|
||||
}
|
||||
}
|
||||
|
||||
// Resource içinde kullanım
|
||||
@Path("/api/sensitive")
|
||||
public class SensitiveResource {
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@GET
|
||||
@RolesAllowed("ADMIN")
|
||||
public Response getData() {
|
||||
auditService.logAccess("sensitive-data", "READ");
|
||||
return Response.ok(data).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bağımlılık Güvenliği Taraması
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
|
||||
# Gradle
|
||||
./gradlew dependencyCheckAnalyze
|
||||
|
||||
# Check Quarkus extensions
|
||||
quarkus extension list --installable
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
- Production'da her zaman HTTPS kullanın
|
||||
- Stateless kimlik doğrulama için JWT veya OIDC etkinleştirin
|
||||
- Bildirimsel yetkilendirme için `@RolesAllowed` kullanın
|
||||
- Bean Validation ile tüm girişleri doğrulayın
|
||||
- Parolaları BCrypt ile hash'leyin (asla düz metin saklamayın)
|
||||
- Gizli bilgileri Vault veya ortam değişkenlerinde saklayın
|
||||
- SQL injection'ı önlemek için parametreli sorgular kullanın
|
||||
- Tüm yanıtlara güvenlik başlıkları ekleyin
|
||||
- Genel endpoint'lerde rate limiting uygulayın
|
||||
- Hassas işlemleri denetleyin
|
||||
- Bağımlılıkları güncel tutun ve CVE için tarayın
|
||||
- Programatik kontroller için SecurityIdentity kullanın
|
||||
- Uygun CORS politikaları belirleyin
|
||||
- Kimlik doğrulama ve yetkilendirme yollarını test edin
|
||||
@@ -0,0 +1,916 @@
|
||||
---
|
||||
name: quarkus-tdd
|
||||
description: Test-driven development for Quarkus 3.x LTS using JUnit 5, Mockito, REST Assured, Camel testing, and JaCoCo. Use when adding features, fixing bugs, or refactoring event-driven services.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Quarkus TDD İş Akışı
|
||||
|
||||
80%+ kapsam (unit + integration) ile Quarkus 3.x servisleri için TDD rehberi. Apache Camel ile event-driven mimariler için optimize edilmiştir.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Yeni özellikler veya REST endpoint'leri
|
||||
- Bug düzeltmeleri veya refactoring'ler
|
||||
- Veri erişim mantığı, güvenlik kuralları veya reaktif akışlar ekleme
|
||||
- Apache Camel route'larını ve event handler'larını test etme
|
||||
- RabbitMQ ile event-driven servisleri test etme
|
||||
- Koşullu akış mantığını test etme
|
||||
- CompletableFuture async işlemlerini doğrulama
|
||||
- LogContext yayılımını test etme
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Önce testleri yazın (başarısız olmalılar)
|
||||
2. Geçmek için minimal kod uygulayın
|
||||
3. Testleri yeşil tutarken refactor edin
|
||||
4. JaCoCo ile kapsamı zorlayın (%80+ hedef)
|
||||
|
||||
## Examples
|
||||
|
||||
### @Nested Organizasyonlu Unit Testler
|
||||
|
||||
Kapsamlı ve okunabilir testler için bu yapılandırılmış yaklaşımı izleyin:
|
||||
|
||||
```java
|
||||
@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;
|
||||
private StoredDocumentInfo documentInfo;
|
||||
|
||||
@BeforeEach
|
||||
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);
|
||||
}
|
||||
|
||||
@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(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),
|
||||
eq("PERSISTENCE_BLOB_EVENT_TYPE"));
|
||||
verify(eventService).createSuccessEvent(any(BusinessRulesPayload.class),
|
||||
eq("BUSINESS_RULES_MESSAGE_SENT"));
|
||||
verify(businessRulesPublisher).publishAsync(any(BusinessRulesPayload.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("CHORUS_FLOW için schematron validasyonu atlanmalı")
|
||||
void givenChorusFlow_whenProcessFile_thenSchematronBypassed() throws Exception {
|
||||
// ARRANGE
|
||||
testLogContext.put(As2Constants.CHORUS_FLOW, "true");
|
||||
CustomLog.setCurrentContext(testLogContext);
|
||||
|
||||
when(invoiceFlowValidator.validateFlowWithConfig(
|
||||
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(),
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Dosya yükleme başarısız olduğunda hata eventi oluşturulmalı")
|
||||
void givenUploadFailure_whenProcessFile_thenErrorEventCreated() throws Exception {
|
||||
// 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"),
|
||||
contains("File path is empty"));
|
||||
|
||||
verify(businessRulesPublisher, never()).publishAsync(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("CompletableFuture.join() başarısızlığı ele alınmalı")
|
||||
void givenAsyncUploadFailure_whenProcessFile_thenExceptionThrown() throws Exception {
|
||||
// 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<StoredDocumentInfo> failedFuture =
|
||||
CompletableFuture.failedFuture(new StorageException("S3 connection failed"));
|
||||
when(fileStorageService.uploadOriginalFile(any(), anyLong(), any(), any()))
|
||||
.thenReturn(failedFuture);
|
||||
|
||||
// ACT & ASSERT
|
||||
assertThrows(
|
||||
CompletionException.class,
|
||||
() -> as2ProcessingService.processFile(testFilePath)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Dosya yolu null olduğunda exception fırlatılmalı")
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Temel Test Desenleri
|
||||
|
||||
1. **@Nested Sınıflar**: Testleri test edilen metoda göre gruplandırın
|
||||
2. **@DisplayName**: Test raporlarında okunabilir açıklamalar sağlayın
|
||||
3. **İsimlendirme Kuralı**: Netlik için `givenX_whenY_thenZ`
|
||||
4. **AAA Deseni**: Açık `// ARRANGE`, `// ACT`, `// ASSERT` yorumları
|
||||
5. **@BeforeEach**: Tekrarı azaltmak için ortak test verisi kurulumu
|
||||
6. **assertDoesNotThrow**: Exception yakalamadan başarı senaryolarını test edin
|
||||
7. **assertThrows**: AssertJ kullanarak mesaj doğrulamalı exception senaryolarını test edin
|
||||
8. **Kapsamlı Kapsam**: Mutlu yolları, null girdileri, edge case'leri, exception'ları test edin
|
||||
9. **Etkileşimleri Doğrulama**: Metodların doğru çağrıldığından emin olmak için Mockito `verify()` kullanın
|
||||
10. **Hiçbir Zaman Doğrulama**: Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `never()` kullanın
|
||||
|
||||
## Camel Route Testi
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@DisplayName("Business Rules Camel Route Tests")
|
||||
class BusinessRulesRouteTest {
|
||||
|
||||
@Inject
|
||||
CamelContext camelContext;
|
||||
|
||||
@Inject
|
||||
ProducerTemplate producerTemplate;
|
||||
|
||||
@InjectMock
|
||||
EventService eventService;
|
||||
|
||||
private BusinessRulesPayload testPayload;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE - Test verisi
|
||||
testPayload = new BusinessRulesPayload();
|
||||
testPayload.setDocumentId(1L);
|
||||
testPayload.setFlowProfile(FlowProfile.BASIC);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("business-rules-publisher route için testler")
|
||||
class BusinessRulesPublisher {
|
||||
|
||||
@Test
|
||||
@DisplayName("Mesajı başarıyla RabbitMQ'ya yayınlamalı")
|
||||
void givenValidPayload_whenPublish_thenMessageSentToQueue() throws Exception {
|
||||
// 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 -> {
|
||||
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 — .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");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JSON'a marshalling'i ele almalı")
|
||||
void givenPayload_whenPublish_thenMarshalledToJson() throws Exception {
|
||||
// ARRANGE
|
||||
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\"");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("document-processing route için testler")
|
||||
class DocumentProcessing {
|
||||
|
||||
@Test
|
||||
@DisplayName("Faturayı doğru işlemciye yönlendirmeli")
|
||||
void givenInvoiceType_whenProcess_thenRoutesToInvoiceProcessor() throws Exception {
|
||||
// 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",
|
||||
testPayload, "documentType", "INVOICE");
|
||||
|
||||
// ASSERT
|
||||
mockInvoice.assertIsSatisfied(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Validasyon hatalarını zarif şekilde ele almalı")
|
||||
void givenValidationError_whenProcess_thenRoutesToErrorHandler() throws Exception {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Service Testi
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("EventService Unit Tests")
|
||||
class EventServiceTest {
|
||||
|
||||
@Mock
|
||||
private EventRepository eventRepository;
|
||||
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@InjectMocks
|
||||
private EventService eventService;
|
||||
|
||||
private BusinessRulesPayload testPayload;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE
|
||||
testPayload = new BusinessRulesPayload();
|
||||
testPayload.setDocumentId(1L);
|
||||
}
|
||||
|
||||
@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(() ->
|
||||
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
|
||||
|
||||
// ASSERT
|
||||
verify(eventRepository).persist(argThat(event ->
|
||||
event.getType().equals("DOCUMENT_PROCESSED") &&
|
||||
event.getStatus() == EventStatus.SUCCESS &&
|
||||
event.getPayload().equals("{\"documentId\":1}") &&
|
||||
event.getTimestamp() != null
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Payload null olduğunda exception fırlatılmalı")
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@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(() ->
|
||||
eventService.createErrorEvent(testPayload, "PROCESSING_ERROR", errorMessage));
|
||||
|
||||
// ASSERT
|
||||
verify(eventRepository).persist(argThat(event ->
|
||||
event.getType().equals("PROCESSING_ERROR") &&
|
||||
event.getStatus() == EventStatus.ERROR &&
|
||||
event.getErrorMessage().equals(errorMessage) &&
|
||||
event.getPayload().equals("{\"documentId\":1}")
|
||||
));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("Geçersiz hata mesajları reddedilmeli")
|
||||
@ValueSource(strings = {"", " "})
|
||||
void givenBlankErrorMessage_whenCreateErrorEvent_thenThrowsException(String blankMessage) {
|
||||
// ACT & ASSERT
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
|
||||
);
|
||||
|
||||
assertThat(exception.getMessage()).contains("Error message cannot be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CompletableFuture Testi
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("FileStorageService Unit Tests")
|
||||
class FileStorageServiceTest {
|
||||
|
||||
@Mock
|
||||
private S3Client s3Client;
|
||||
|
||||
@Mock
|
||||
private ExecutorService executorService;
|
||||
|
||||
@InjectMocks
|
||||
private FileStorageService fileStorageService;
|
||||
|
||||
private InputStream testInputStream;
|
||||
private LogContext testLogContext;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ARRANGE
|
||||
testInputStream = new ByteArrayInputStream("test content".getBytes());
|
||||
testLogContext = new LogContext();
|
||||
testLogContext.put("traceId", "trace-123");
|
||||
}
|
||||
|
||||
@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 {
|
||||
// ARRANGE
|
||||
doAnswer(invocation -> {
|
||||
((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<StoredDocumentInfo> 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));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("S3 yükleme başarısızlığını ele almalı")
|
||||
void givenS3Failure_whenUpload_thenCompletableFutureFails() {
|
||||
// ARRANGE
|
||||
doAnswer(invocation -> {
|
||||
((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<StoredDocumentInfo> future =
|
||||
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
|
||||
testLogContext, InvoiceFormat.UBL);
|
||||
|
||||
// ASSERT
|
||||
assertThatThrownBy(() -> future.join())
|
||||
.isInstanceOf(CompletionException.class)
|
||||
.hasCauseInstanceOf(StorageException.class)
|
||||
.hasMessageContaining("S3 unavailable");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("LogContext'i async işleme yaymalı")
|
||||
void givenLogContext_whenUpload_thenContextPropagated() throws Exception {
|
||||
// ARRANGE
|
||||
AtomicReference<LogContext> 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,
|
||||
testLogContext, InvoiceFormat.UBL).join();
|
||||
|
||||
// ASSERT
|
||||
assertThat(capturedContext.get()).isNotNull();
|
||||
assertThat(capturedContext.get().get("traceId")).isEqualTo("trace-123");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resource Katmanı Testleri (REST Assured)
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@DisplayName("DocumentResource API Tests")
|
||||
class DocumentResourceTest {
|
||||
|
||||
@InjectMock
|
||||
DocumentService documentService;
|
||||
|
||||
@Nested
|
||||
@DisplayName("GET /api/documents için testler")
|
||||
class ListDocuments {
|
||||
|
||||
@Test
|
||||
@DisplayName("Belge listesini döndürmeli")
|
||||
void givenDocumentsExist_whenList_thenReturnsOk() {
|
||||
// ARRANGE
|
||||
List<Document> documents = List.of(createDocument(1L, "DOC-001"));
|
||||
when(documentService.list(0, 20)).thenReturn(documents);
|
||||
|
||||
// ACT & ASSERT
|
||||
given()
|
||||
.when().get("/api/documents")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("$.size()", is(1))
|
||||
.body("[0].referenceNumber", equalTo("DOC-001"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/documents için testler")
|
||||
class CreateDocument {
|
||||
|
||||
@Test
|
||||
@DisplayName("Belge oluşturmalı ve 201 döndürmeli")
|
||||
void givenValidRequest_whenCreate_thenReturns201() {
|
||||
// ARRANGE
|
||||
Document document = createDocument(1L, "DOC-001");
|
||||
when(documentService.create(any())).thenReturn(document);
|
||||
|
||||
// ACT & ASSERT
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"referenceNumber": "DOC-001",
|
||||
"description": "Test document",
|
||||
"validUntil": "2030-01-01T00:00:00Z",
|
||||
"categories": ["test"]
|
||||
}
|
||||
""")
|
||||
.when().post("/api/documents")
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.header("Location", containsString("/api/documents/1"))
|
||||
.body("referenceNumber", equalTo("DOC-001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Geçersiz girdi için 400 döndürmeli")
|
||||
void givenInvalidRequest_whenCreate_thenReturns400() {
|
||||
// ACT & ASSERT
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"referenceNumber": "",
|
||||
"description": "Test"
|
||||
}
|
||||
""")
|
||||
.when().post("/api/documents")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
}
|
||||
|
||||
private Document createDocument(Long id, String referenceNumber) {
|
||||
Document document = new Document();
|
||||
document.setId(id);
|
||||
document.setReferenceNumber(referenceNumber);
|
||||
document.setStatus(DocumentStatus.PENDING);
|
||||
return document;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gerçek Veritabanıyla Entegrasyon Testleri
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@TestProfile(IntegrationTestProfile.class)
|
||||
@DisplayName("Document Integration Tests")
|
||||
class DocumentIntegrationTest {
|
||||
|
||||
@Test
|
||||
@Transactional
|
||||
@DisplayName("Belge oluşturulmalı ve API üzerinden alınabilmeli")
|
||||
void givenNewDocument_whenCreateAndRetrieve_thenSuccessful() {
|
||||
// ACT - API üzerinden oluştur
|
||||
Long id = given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{
|
||||
"referenceNumber": "INT-001",
|
||||
"description": "Integration test",
|
||||
"validUntil": "2030-01-01T00:00:00Z",
|
||||
"categories": ["test"]
|
||||
}
|
||||
""")
|
||||
.when().post("/api/documents")
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.extract().path("id");
|
||||
|
||||
// ASSERT - API üzerinden al
|
||||
given()
|
||||
.when().get("/api/documents/" + id)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("referenceNumber", equalTo("INT-001"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JaCoCo ile Kapsam
|
||||
|
||||
### Maven Yapılandırması (Tam)
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.13</version>
|
||||
<executions>
|
||||
<!-- Test yürütmesi için agent'ı hazırla -->
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
|
||||
<!-- Kapsam raporu oluştur -->
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
|
||||
<!-- Kapsam eşiklerini zorla -->
|
||||
<execution>
|
||||
<id>check</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>LINE</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.80</minimum>
|
||||
</limit>
|
||||
<limit>
|
||||
<counter>BRANCH</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>0.70</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
Kapsam ile testleri çalıştırın:
|
||||
```bash
|
||||
mvn clean test
|
||||
mvn jacoco:report
|
||||
mvn jacoco:check
|
||||
|
||||
# Rapor: target/site/jacoco/index.html
|
||||
```
|
||||
|
||||
## Test Bağımlılıkları
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Quarkus Test -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Mockito -->
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AssertJ (JUnit assertion'larına tercih edilir) -->
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.24.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- REST Assured -->
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Camel Test -->
|
||||
<dependency>
|
||||
<groupId>org.apache.camel.quarkus</groupId>
|
||||
<artifactId>camel-quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
### Test Organizasyonu
|
||||
- Testleri test edilen metoda göre gruplandırmak için `@Nested` sınıflar kullanın
|
||||
- Raporlarda görünür okunabilir açıklamalar için `@DisplayName` kullanın
|
||||
- Test metodları için `givenX_whenY_thenZ` isimlendirme kuralını izleyin
|
||||
- Tekrarı azaltmak için ortak test verisi kurulumunda `@BeforeEach` kullanın
|
||||
|
||||
### Test Yapısı
|
||||
- Açık yorumlarla AAA desenini izleyin (`// ARRANGE`, `// ACT`, `// ASSERT`)
|
||||
- Başarı senaryoları için `assertDoesNotThrow` kullanın
|
||||
- Mesaj doğrulamalı exception senaryoları için `assertThrows` kullanın
|
||||
- AssertJ `contains()` veya `isEqualTo()` kullanarak exception mesajlarının beklenen değerlerle eşleştiğini doğrulayın
|
||||
|
||||
### Test Kapsamı
|
||||
- Tüm public metodlar için mutlu yolları test edin
|
||||
- Null girdi işlemeyi test edin
|
||||
- Edge case'leri test edin (boş koleksiyonlar, sınır değerleri, negatif ID'ler, boş string'ler)
|
||||
- Exception senaryolarını kapsamlı biçimde test edin
|
||||
- Tüm harici bağımlılıkları mock'layın (repository'ler, servisler, Camel endpoint'leri)
|
||||
- %80+ satır kapsamı, %70+ branch kapsamı hedefleyin
|
||||
|
||||
### Assertion'lar
|
||||
- Değer kontrolleri için JUnit assertion'ları yerine **AssertJ'yi tercih edin** (`assertThat`)
|
||||
- Okunabilirlik için akıcı AssertJ API'si kullanın: `assertThat(list).hasSize(3).contains(item)`
|
||||
- Exception'lar için: JUnit `assertThrows` ile yakalayın, ardından AssertJ ile mesajı doğrulayın
|
||||
- Fırlatılmayan başarı yolları için: JUnit `assertDoesNotThrow` kullanın
|
||||
- Koleksiyonlar için: `extracting()`, `filteredOn()`, `containsExactly()`
|
||||
|
||||
### Entegrasyon Testi
|
||||
- Entegrasyon testleri için `@QuarkusTest` kullanın
|
||||
- Quarkus testlerinde bağımlılıkları mock'lamak için `@InjectMock` kullanın
|
||||
- API testi için REST Assured'ı tercih edin
|
||||
- Test'e özel yapılandırma için `@TestProfile` kullanın
|
||||
|
||||
### Event-Driven Test
|
||||
- `AdviceWith` ve `MockEndpoint` ile Camel route'larını test edin
|
||||
- `@CamelQuarkusTest` annotasyonu kullanın (bağımsız Camel testleri kullanıyorsanız)
|
||||
- Mesaj içeriğini, başlıklarını ve yönlendirme mantığını doğrulayın
|
||||
- Hata işleme route'larını ayrı ayrı test edin
|
||||
- Unit testlerde harici sistemleri (RabbitMQ, S3, veritabanları) mock'layın
|
||||
|
||||
### Camel Route Testi
|
||||
- Mesaj akışını doğrulamak için `MockEndpoint` kullanın
|
||||
- Test için route'ları değiştirmek üzere `AdviceWith` kullanın (endpoint'leri mock'larla değiştirin)
|
||||
- Mesaj dönüşümünü ve marshalling'i test edin
|
||||
- Exception işleme ve dead letter queue'ları test edin
|
||||
|
||||
### Async İşlem Testi
|
||||
- CompletableFuture başarı ve başarısızlık senaryolarını test edin
|
||||
- Async tamamlanmayı beklemek için testlerde `.join()` kullanın
|
||||
- CompletableFuture'dan exception yayılımını test edin
|
||||
- LogContext yayılımını async işlemlere doğrulayın
|
||||
|
||||
### Performans
|
||||
- Testleri hızlı ve izole tutun
|
||||
- Testleri sürekli modda çalıştırın: `mvn quarkus:test`
|
||||
- Girdi varyasyonları için parametreli testler (`@ParameterizedTest`) kullanın
|
||||
- Yeniden kullanılabilir test verisi builder'ları veya factory metodları oluşturun
|
||||
|
||||
### Quarkus'a Özgü
|
||||
- En son LTS sürümünde kalın (Quarkus 3.x)
|
||||
- Native derleme uyumluluğunu periyodik olarak test edin
|
||||
- Farklı senaryolar için Quarkus test profillerini kullanın
|
||||
- Yerel test için Quarkus dev servislerinden yararlanın
|
||||
- `@MockBean` yerine `@InjectMock` kullanın (Quarkus'a özgü)
|
||||
|
||||
### Doğrulama En İyi Uygulamaları
|
||||
- Mock'lanmış bağımlılıklardaki etkileşimleri her zaman doğrulayın
|
||||
- Hata senaryolarında metodların ÇAĞRILMADIĞINI sağlamak için `verify(mock, never())` kullanın
|
||||
- Karmaşık argüman eşleştirme için `argThat()` kullanın
|
||||
- Önem taşıdığında çağrı sırasını doğrulayın: Mockito'dan `InOrder`
|
||||
@@ -0,0 +1,479 @@
|
||||
---
|
||||
name: quarkus-verification
|
||||
description: "Verification loop for Quarkus projects: build, static analysis, tests with coverage, security scans, native compilation, and diff review before release or PR."
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Quarkus Doğrulama Döngüsü
|
||||
|
||||
PR'lardan önce, büyük değişikliklerden sonra ve deployment öncesi çalıştırın.
|
||||
|
||||
## Ne Zaman Aktif Edilir
|
||||
|
||||
- Quarkus servisi için pull request açmadan önce
|
||||
- Büyük refactoring veya bağımlılık yükseltmelerinden sonra
|
||||
- Staging veya production için deployment öncesi doğrulama
|
||||
- Tam build → lint → test → güvenlik taraması → native derleme pipeline'ı çalıştırma
|
||||
- Test kapsamının eşikleri karşıladığını doğrulama (%80+)
|
||||
- Native image uyumluluğunu test etme
|
||||
|
||||
## Faz 1: Build
|
||||
|
||||
```bash
|
||||
# Maven
|
||||
mvn clean verify -DskipTests
|
||||
|
||||
# Gradle
|
||||
./gradlew clean assemble -x test
|
||||
```
|
||||
|
||||
Build başarısız olursa, durdurun ve derleme hatalarını düzeltin.
|
||||
|
||||
## Faz 2: Static Analiz
|
||||
|
||||
### Checkstyle, PMD, SpotBugs (Maven)
|
||||
|
||||
```bash
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
```
|
||||
|
||||
### SonarQube (yapılandırılmışsa)
|
||||
|
||||
```bash
|
||||
mvn sonar:sonar \
|
||||
-Dsonar.projectKey=my-quarkus-project \
|
||||
-Dsonar.host.url=http://localhost:9000 \
|
||||
-Dsonar.login=${SONAR_TOKEN}
|
||||
```
|
||||
|
||||
### Ele Alınacak Yaygın Sorunlar
|
||||
|
||||
- Kullanılmayan import'lar veya değişkenler
|
||||
- Karmaşık metodlar (yüksek cyclomatic complexity)
|
||||
- Potansiyel null pointer dereference'ları
|
||||
- SpotBugs tarafından işaretlenen güvenlik sorunları
|
||||
|
||||
## Faz 3: Testler + Kapsam
|
||||
|
||||
```bash
|
||||
# Tüm testleri çalıştır
|
||||
mvn clean test
|
||||
|
||||
# Kapsam raporu oluştur
|
||||
mvn jacoco:report
|
||||
|
||||
# Kapsam eşiğini zorla (%80)
|
||||
mvn jacoco:check
|
||||
|
||||
# Veya Gradle ile
|
||||
./gradlew test jacocoTestReport jacocoTestCoverageVerification
|
||||
```
|
||||
|
||||
### Test Kategorileri
|
||||
|
||||
#### Unit Testler
|
||||
Mock'lanmış bağımlılıklarla servis mantığını test edin:
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceTest {
|
||||
@Mock UserRepository userRepository;
|
||||
@InjectMocks UserService userService;
|
||||
|
||||
@Test
|
||||
void createUser_validInput_returnsUser() {
|
||||
var dto = new CreateUserDto("Alice", "alice@example.com");
|
||||
|
||||
// Panache persist() void döndürür — doNothing + verify kullanın
|
||||
doNothing().when(userRepository).persist(any(User.class));
|
||||
|
||||
User result = userService.create(dto);
|
||||
|
||||
assertThat(result.name).isEqualTo("Alice");
|
||||
verify(userRepository).persist(any(User.class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Entegrasyon Testleri
|
||||
Gerçek veritabanıyla (Testcontainers) test edin:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
@QuarkusTestResource(PostgresTestResource.class)
|
||||
class UserRepositoryIntegrationTest {
|
||||
|
||||
@Inject
|
||||
UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
@Transactional
|
||||
void findByEmail_existingUser_returnsUser() {
|
||||
User user = new User();
|
||||
user.name = "Alice";
|
||||
user.email = "alice@example.com";
|
||||
userRepository.persist(user);
|
||||
|
||||
Optional<User> found = userRepository.findByEmail("alice@example.com");
|
||||
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().name).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### API Testleri
|
||||
REST Assured ile REST endpoint'lerini test edin:
|
||||
|
||||
```java
|
||||
@QuarkusTest
|
||||
class UserResourceTest {
|
||||
|
||||
@Test
|
||||
void createUser_validInput_returns201() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{"name": "Alice", "email": "alice@example.com"}
|
||||
""")
|
||||
.when().post("/api/users")
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.body("name", equalTo("Alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_invalidEmail_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("""
|
||||
{"name": "Alice", "email": "invalid"}
|
||||
""")
|
||||
.when().post("/api/users")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kapsam Raporu
|
||||
|
||||
Ayrıntılı kapsam için `target/site/jacoco/index.html` sayfasını kontrol edin:
|
||||
- Genel satır kapsamı (hedef: %80+)
|
||||
- Branch kapsamı (hedef: %70+)
|
||||
- Kapsanmamış kritik yolları belirleyin
|
||||
|
||||
## Faz 4: Güvenlik Taraması
|
||||
|
||||
### Bağımlılık Güvenlik Açıkları (Maven)
|
||||
|
||||
```bash
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
```
|
||||
|
||||
CVE'ler için `target/dependency-check-report.html` raporunu inceleyin.
|
||||
|
||||
### Quarkus Güvenlik Denetimi
|
||||
|
||||
```bash
|
||||
# Güvenlik açığı olan extension'ları kontrol et
|
||||
mvn quarkus:audit
|
||||
|
||||
# Tüm extension'ları listele
|
||||
mvn quarkus:list-extensions
|
||||
```
|
||||
|
||||
### OWASP ZAP (API Güvenlik Testi)
|
||||
|
||||
```bash
|
||||
docker run -t owasp/zap2docker-stable zap-api-scan.py \
|
||||
-t http://localhost:8080/q/openapi \
|
||||
-f openapi
|
||||
```
|
||||
|
||||
### Yaygın Güvenlik Kontrolleri
|
||||
|
||||
- [ ] Tüm gizli bilgiler ortam değişkenlerinde (kodda değil)
|
||||
- [ ] Tüm endpoint'lerde girdi doğrulama
|
||||
- [ ] Kimlik doğrulama/yetkilendirme yapılandırılmış
|
||||
- [ ] CORS düzgün yapılandırılmış
|
||||
- [ ] Güvenlik başlıkları ayarlanmış
|
||||
- [ ] Parolalar BCrypt ile hash'lenmiş
|
||||
- [ ] SQL injection koruması (parametreli sorgular)
|
||||
- [ ] Genel endpoint'lerde rate limiting
|
||||
|
||||
## Faz 5: Native Derleme
|
||||
|
||||
GraalVM native image uyumluluğunu test edin:
|
||||
|
||||
```bash
|
||||
# Native executable oluştur
|
||||
mvn package -Dnative
|
||||
|
||||
# Veya container ile
|
||||
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||
|
||||
# Native executable'ı test et
|
||||
./target/*-runner
|
||||
|
||||
# Temel smoke testleri çalıştır
|
||||
curl http://localhost:8080/q/health/live
|
||||
curl http://localhost:8080/q/health/ready
|
||||
```
|
||||
|
||||
### Native Image Sorun Giderme
|
||||
|
||||
Yaygın sorunlar:
|
||||
- **Reflection**: Dinamik sınıflar için reflection yapılandırması ekleyin
|
||||
- **Resources**: `quarkus.native.resources.includes` ile kaynakları dahil edin
|
||||
- **JNI**: Native kütüphaneler kullanıyorsanız JNI sınıflarını kaydedin
|
||||
|
||||
Örnek reflection yapılandırması:
|
||||
```java
|
||||
@RegisterForReflection(targets = {MyDynamicClass.class})
|
||||
public class ReflectionConfiguration {}
|
||||
```
|
||||
|
||||
## Faz 6: Performans Testi
|
||||
|
||||
### K6 ile Yük Testi
|
||||
|
||||
```javascript
|
||||
// load-test.js
|
||||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '30s', target: 50 },
|
||||
{ duration: '1m', target: 100 },
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
};
|
||||
|
||||
export default function () {
|
||||
const res = http.get('http://localhost:8080/api/markets');
|
||||
check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'response time < 200ms': (r) => r.timings.duration < 200,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Çalıştırın:
|
||||
```bash
|
||||
k6 run load-test.js
|
||||
```
|
||||
|
||||
### İzlenecek Metrikler
|
||||
|
||||
- Yanıt süresi (p50, p95, p99)
|
||||
- Throughput (istek/saniye)
|
||||
- Hata oranı
|
||||
- Bellek kullanımı
|
||||
- CPU kullanımı
|
||||
|
||||
## Faz 7: Sağlık Kontrolleri
|
||||
|
||||
```bash
|
||||
# Liveness
|
||||
curl http://localhost:8080/q/health/live
|
||||
|
||||
# Readiness
|
||||
curl http://localhost:8080/q/health/ready
|
||||
|
||||
# Tüm sağlık kontrolleri
|
||||
curl http://localhost:8080/q/health
|
||||
|
||||
# Metrikler (etkinleştirilmişse)
|
||||
curl http://localhost:8080/q/metrics
|
||||
```
|
||||
|
||||
Beklenen yanıtlar:
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"checks": [
|
||||
{
|
||||
"name": "Database connection",
|
||||
"status": "UP"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Faz 8: Container Image Build
|
||||
|
||||
```bash
|
||||
# Container image oluştur
|
||||
mvn package -Dquarkus.container-image.build=true
|
||||
|
||||
# Veya belirli registry ile
|
||||
mvn package \
|
||||
-Dquarkus.container-image.build=true \
|
||||
-Dquarkus.container-image.registry=docker.io \
|
||||
-Dquarkus.container-image.group=myorg \
|
||||
-Dquarkus.container-image.tag=1.0.0
|
||||
|
||||
# Container'ı test et
|
||||
docker run -p 8080:8080 myorg/my-quarkus-app:1.0.0
|
||||
```
|
||||
|
||||
### Container Güvenlik Taraması
|
||||
|
||||
```bash
|
||||
# Trivy
|
||||
trivy image myorg/my-quarkus-app:1.0.0
|
||||
|
||||
# Grype
|
||||
grype myorg/my-quarkus-app:1.0.0
|
||||
```
|
||||
|
||||
## Faz 9: Yapılandırma Doğrulama
|
||||
|
||||
```bash
|
||||
# Tüm yapılandırma özelliklerini kontrol et
|
||||
mvn quarkus:info
|
||||
|
||||
# Tüm yapılandırma kaynaklarını listele
|
||||
curl http://localhost:8080/q/dev/io.quarkus.quarkus-vertx-http/config
|
||||
```
|
||||
|
||||
### Ortama Özgü Kontroller
|
||||
|
||||
- [ ] Veritabanı URL'leri ortam başına yapılandırılmış
|
||||
- [ ] Gizli bilgiler dışsallaştırılmış (Vault, ortam değişkenleri)
|
||||
- [ ] Loglama seviyeleri uygun
|
||||
- [ ] CORS origin'leri doğru ayarlanmış
|
||||
- [ ] Rate limiting yapılandırılmış
|
||||
- [ ] İzleme/tracing etkinleştirilmiş
|
||||
|
||||
## Faz 10: Dokümantasyon İncelemesi
|
||||
|
||||
- [ ] OpenAPI/Swagger dokümanları güncel (`/q/swagger-ui`)
|
||||
- [ ] README kurulum talimatlarını içeriyor
|
||||
- [ ] API değişiklikleri belgelenmiş
|
||||
- [ ] Breaking change'ler için migration rehberi
|
||||
- [ ] Yapılandırma özellikleri belgelenmiş
|
||||
|
||||
OpenAPI spec oluşturun:
|
||||
```bash
|
||||
curl http://localhost:8080/q/openapi -o openapi.json
|
||||
```
|
||||
|
||||
## Doğrulama Kontrol Listesi
|
||||
|
||||
### Kod Kalitesi
|
||||
- [ ] Build uyarısız geçiyor
|
||||
- [ ] Static analiz temiz (yüksek/orta sorun yok)
|
||||
- [ ] Kod ekip kurallarını takip ediyor
|
||||
- [ ] PR'da yorum satırına alınmış kod veya TODO yok
|
||||
|
||||
### Test
|
||||
- [ ] Tüm testler geçiyor
|
||||
- [ ] Kod kapsamı ≥ %80
|
||||
- [ ] Gerçek veritabanıyla entegrasyon testleri
|
||||
- [ ] Güvenlik testleri geçiyor
|
||||
- [ ] Performans kabul edilebilir sınırlar içinde
|
||||
|
||||
### Güvenlik
|
||||
- [ ] Bağımlılık güvenlik açığı yok
|
||||
- [ ] Kimlik doğrulama/yetkilendirme test edilmiş
|
||||
- [ ] Girdi doğrulama tamamlanmış
|
||||
- [ ] Gizli bilgiler kaynak kodda değil
|
||||
- [ ] Güvenlik başlıkları yapılandırılmış
|
||||
|
||||
### Deployment
|
||||
- [ ] Native derleme başarılı
|
||||
- [ ] Container image oluşturuluyor
|
||||
- [ ] Sağlık kontrolleri doğru yanıt veriyor
|
||||
- [ ] Hedef ortam için yapılandırma geçerli
|
||||
|
||||
### Native Image
|
||||
- [ ] Native executable oluşturuluyor
|
||||
- [ ] Native testler geçiyor
|
||||
- [ ] Başlangıç süresi < 100ms
|
||||
- [ ] Bellek ayak izi kabul edilebilir
|
||||
|
||||
## Otomatik Doğrulama Script'i
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Faz 1: Build ==="
|
||||
mvn clean verify -DskipTests
|
||||
|
||||
echo "=== Faz 2: Static Analiz ==="
|
||||
mvn checkstyle:check pmd:check spotbugs:check
|
||||
|
||||
echo "=== Faz 3: Testler + Kapsam ==="
|
||||
mvn test jacoco:report jacoco:check
|
||||
|
||||
echo "=== Faz 4: Güvenlik Taraması ==="
|
||||
mvn org.owasp:dependency-check-maven:check
|
||||
|
||||
echo "=== Faz 5: Native Derleme ==="
|
||||
mvn package -Dnative -Dquarkus.native.container-build=true
|
||||
|
||||
echo "=== Tüm Fazlar Tamamlandı ==="
|
||||
echo "Raporları inceleyin:"
|
||||
echo " - Kapsam: target/site/jacoco/index.html"
|
||||
echo " - Güvenlik: target/dependency-check-report.html"
|
||||
echo " - Native: target/*-runner"
|
||||
```
|
||||
|
||||
## CI/CD Entegrasyonu
|
||||
|
||||
### GitHub Actions Örneği
|
||||
|
||||
```yaml
|
||||
name: Verification
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
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:
|
||||
files: target/site/jacoco/jacoco.xml
|
||||
```
|
||||
|
||||
## En İyi Uygulamalar
|
||||
|
||||
- Her PR öncesi doğrulama döngüsünü çalıştırın
|
||||
- CI/CD pipeline'ında otomatize edin
|
||||
- Sorunları hemen düzeltin; borç biriktirmeyin
|
||||
- Kapsamı %80'in üzerinde tutun
|
||||
- Bağımlılıkları düzenli olarak güncelleyin
|
||||
- Native derlemeyi periyodik olarak test edin
|
||||
- Performans trendlerini izleyin
|
||||
- Breaking change'leri belgeleyin
|
||||
- Güvenlik tarama sonuçlarını inceleyin
|
||||
- Her ortam için yapılandırmayı doğrulayın
|
||||
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — 智能体指令
|
||||
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 56 个专业代理、217 项技能、72 条命令以及自动化钩子工作流,用于软件开发。
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 58 个专业代理、220 项技能、74 条命令以及自动化钩子工作流,用于软件开发。
|
||||
|
||||
**版本:** 2.0.0-rc.1
|
||||
|
||||
@@ -146,9 +146,9 @@
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
agents/ — 56 个专业子代理
|
||||
skills/ — 217 个工作流技能和领域知识
|
||||
commands/ — 72 个斜杠命令
|
||||
agents/ — 58 个专业子代理
|
||||
skills/ — 220 个工作流技能和领域知识
|
||||
commands/ — 74 个斜杠命令
|
||||
hooks/ — 基于触发的自动化
|
||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||
scripts/ — 跨平台 Node.js 实用工具
|
||||
|
||||
+12
-8
@@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**搞定!** 你现在可以使用 56 个智能体、217 项技能和 72 个命令了。
|
||||
**搞定!** 你现在可以使用 58 个智能体、220 项技能和 74 个命令了。
|
||||
|
||||
***
|
||||
|
||||
@@ -348,6 +348,10 @@ everything-claude-code/
|
||||
| |-- laravel-verification/ # Laravel 验证循环(新增)
|
||||
| |-- python-patterns/ # Python 习惯用法与最佳实践(新增)
|
||||
| |-- python-testing/ # 使用 pytest 的 Python 测试(新增)
|
||||
| |-- quarkus-patterns/ # Java Quarkus 模式(新增)
|
||||
| |-- quarkus-security/ # Quarkus 安全(新增)
|
||||
| |-- quarkus-tdd/ # Quarkus TDD(新增)
|
||||
| |-- quarkus-verification/ # Quarkus 验证(新增)
|
||||
| |-- springboot-patterns/ # Java Spring Boot 模式(新增)
|
||||
| |-- springboot-security/ # Spring Boot 安全(新增)
|
||||
| |-- springboot-tdd/ # Spring Boot TDD(新增)
|
||||
@@ -677,7 +681,7 @@ cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/
|
||||
cp -r everything-claude-code/skills/search-first ~/.claude/skills/
|
||||
|
||||
# Optional: add niche/framework-specific skills only when needed
|
||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do
|
||||
# for s in django-patterns django-tdd laravel-patterns springboot-patterns quarkus-patterns; do
|
||||
# cp -r everything-claude-code/skills/$s ~/.claude/skills/
|
||||
# done
|
||||
```
|
||||
@@ -1132,9 +1136,9 @@ opencode
|
||||
|
||||
| 功能特性 | Claude Code | OpenCode | 状态 |
|
||||
|---------|-------------|----------|--------|
|
||||
| 智能体 | PASS: 56 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||
| 命令 | PASS: 72 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 217 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 智能体 | PASS: 58 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||
| 命令 | PASS: 74 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 220 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||
@@ -1240,9 +1244,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
||||
|
||||
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||
|---------|------------|------------|-----------|----------|
|
||||
| **智能体** | 56 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||
| **命令** | 72 | 共享 | 基于指令 | 35 |
|
||||
| **技能** | 217 | 共享 | 10 (原生格式) | 37 |
|
||||
| **智能体** | 58 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||
| **命令** | 74 | 共享 | 基于指令 | 35 |
|
||||
| **技能** | 220 | 共享 | 10 (原生格式) | 37 |
|
||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||
|
||||
@@ -151,4 +151,6 @@ grep -A5 "annotationProcessorPaths\|annotationProcessor" pom.xml build.gradle
|
||||
|
||||
最终:`Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list`
|
||||
|
||||
有关详细的 Java 和 Spring Boot 模式,请参阅 `skill: springboot-patterns`。
|
||||
有关详细的模式和示例:
|
||||
* **[SPRING]**:请参阅 `skill: springboot-patterns`
|
||||
* **[QUARKUS]**:请参阅 `skill: quarkus-patterns`
|
||||
|
||||
@@ -102,4 +102,6 @@ grep -rn "FetchType.EAGER" src/main/java --include="*.java"
|
||||
* **警告**:仅存在**中**优先级问题
|
||||
* **阻止**:发现**关键**或**高**优先级问题
|
||||
|
||||
有关详细的Spring Boot模式和示例,请参阅 `skill: springboot-patterns`。
|
||||
有关详细的模式和示例:
|
||||
* **[SPRING]**:请参阅 `skill: springboot-patterns`
|
||||
* **[QUARKUS]**:请参阅 `skill: quarkus-patterns`
|
||||
|
||||
@@ -144,4 +144,5 @@ public record ApiResponse<T>(boolean success, T data, String error) {
|
||||
## 参考
|
||||
|
||||
有关 Spring Boot 架构模式,请参见技能:`springboot-patterns`。
|
||||
有关使用 Camel 和 Panache 的 Quarkus 架构模式,请参见技能:`quarkus-patterns`。
|
||||
有关实体设计和查询优化,请参见技能:`jpa-patterns`。
|
||||
|
||||
@@ -98,4 +98,5 @@ try {
|
||||
## 参考
|
||||
|
||||
关于 Spring Security 认证与授权模式,请参见技能:`springboot-security`。
|
||||
关于使用 JWT/OIDC、RBAC 和 CDI 的 Quarkus 安全模式,请参见技能:`quarkus-security`。
|
||||
关于通用安全检查清单,请参见技能:`security-review`。
|
||||
|
||||
@@ -113,6 +113,7 @@ class OrderRepositoryIT {
|
||||
```
|
||||
|
||||
关于 Spring Boot 集成测试,请参阅技能:`springboot-tdd`。
|
||||
关于 Quarkus 集成测试,请参阅技能:`quarkus-tdd`。
|
||||
|
||||
## 测试命名
|
||||
|
||||
@@ -130,4 +131,5 @@ class OrderRepositoryIT {
|
||||
## 参考
|
||||
|
||||
关于使用 MockMvc 和 Testcontainers 的 Spring Boot TDD 模式,请参阅技能:`springboot-tdd`。
|
||||
关于使用 REST Assured 和 Camel 测试的 Quarkus TDD 模式,请参阅技能:`quarkus-tdd`。
|
||||
关于测试期望,请参阅技能:`java-coding-standards`。
|
||||
|
||||
@@ -126,6 +126,10 @@ mkdir -p $TARGET/skills $TARGET/rules
|
||||
| `java-coding-standards` | Spring Boot 的 Java 编码标准:命名、不可变性、Optional、流 |
|
||||
| `python-patterns` | Pythonic 惯用法、PEP 8、类型提示、最佳实践 |
|
||||
| `python-testing` | 使用 pytest、TDD、夹具、模拟、参数化进行 Python 测试 |
|
||||
| `quarkus-patterns` | Quarkus 架构、使用 Camel 的事件驱动模式、Panache 数据访问、CDI 服务 |
|
||||
| `quarkus-security` | Quarkus 安全:JWT/OIDC 认证、RBAC、Bean 验证、CORS、密钥管理 |
|
||||
| `quarkus-tdd` | 使用 JUnit 5、Mockito、REST Assured、Camel 测试进行 Quarkus TDD |
|
||||
| `quarkus-verification` | Quarkus 验证:构建、静态分析、测试、安全扫描、原生编译 |
|
||||
| `springboot-patterns` | Spring Boot 架构、REST API、分层服务、缓存、异步处理 |
|
||||
| `springboot-security` | Spring Security:认证/授权、验证、CSRF、密钥、速率限制 |
|
||||
| `springboot-tdd` | 使用 JUnit 5、Mockito、MockMvc、Testcontainers 进行 Spring Boot TDD |
|
||||
@@ -285,6 +289,7 @@ grep -rn "skills/" $TARGET/skills/
|
||||
|
||||
* `django-tdd` 可能会引用 `django-patterns`
|
||||
* `laravel-tdd` 可能会引用 `laravel-patterns`
|
||||
* `quarkus-tdd` 可能会引用 `quarkus-patterns`
|
||||
* `springboot-tdd` 可能会引用 `springboot-patterns`
|
||||
* `continuous-learning-v2` 引用 `~/.claude/homunculus/` 目录
|
||||
* `python-testing` 可能会引用 `python-patterns`
|
||||
|
||||
@@ -52,7 +52,7 @@ metadata:
|
||||
* `go.mod` → Go
|
||||
* `pyproject.toml` / `requirements.txt` → Python
|
||||
* `Cargo.toml` → Rust
|
||||
* `build.gradle` / `pom.xml` → Java / Kotlin / Spring Boot
|
||||
* `build.gradle` / `pom.xml` → Java / Kotlin(然后检查构建文件中的`quarkus` → Quarkus,或`spring-boot` → Spring Boot)
|
||||
* `Package.swift` → Swift
|
||||
* `Gemfile` → Ruby
|
||||
* `composer.json` → PHP
|
||||
@@ -116,7 +116,8 @@ metadata:
|
||||
|------------|--------------|-------|
|
||||
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
||||
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | java-reviewer |
|
||||
| Quarkus / Java | quarkus-patterns, quarkus-tdd, quarkus-security, quarkus-verification, java-coding-standards, jpa-patterns | java-reviewer |
|
||||
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
||||
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
||||
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"scripts/doctor.js",
|
||||
"scripts/ecc.js",
|
||||
"scripts/gemini-adapt-agents.js",
|
||||
"scripts/harness-adapter-compliance.js",
|
||||
"scripts/harness-audit.js",
|
||||
"scripts/observability-readiness.js",
|
||||
"scripts/hooks/",
|
||||
@@ -276,6 +277,7 @@
|
||||
"catalog:check": "node scripts/ci/catalog.js --text",
|
||||
"catalog:sync": "node scripts/ci/catalog.js --write --text",
|
||||
"lint": "eslint . && markdownlint '**/*.md' --ignore node_modules",
|
||||
"harness:adapters": "node scripts/harness-adapter-compliance.js",
|
||||
"harness:audit": "node scripts/harness-audit.js",
|
||||
"observability:ready": "node scripts/observability-readiness.js",
|
||||
"claw": "node scripts/claw.js",
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const {
|
||||
ADAPTER_RECORDS,
|
||||
renderMarkdownTable,
|
||||
validateAdapterRecords,
|
||||
validateDocumentation,
|
||||
} = require('./lib/harness-adapter-compliance');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const parsed = {
|
||||
check: false,
|
||||
format: 'text',
|
||||
help: false,
|
||||
root: process.cwd(),
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--check') {
|
||||
parsed.check = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--format') {
|
||||
parsed.format = String(args[index + 1] || '').toLowerCase();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--format=')) {
|
||||
parsed.format = arg.slice('--format='.length).toLowerCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--root') {
|
||||
parsed.root = path.resolve(args[index + 1] || process.cwd());
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--root=')) {
|
||||
parsed.root = path.resolve(arg.slice('--root='.length));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
if (!['text', 'json', 'markdown'].includes(parsed.format)) {
|
||||
throw new Error(`Invalid format: ${parsed.format}. Use text, json, or markdown.`);
|
||||
}
|
||||
|
||||
parsed.root = path.resolve(parsed.root);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log([
|
||||
'Usage: node scripts/harness-adapter-compliance.js [options]',
|
||||
'',
|
||||
'Validate or render the ECC harness adapter compliance scorecard.',
|
||||
'',
|
||||
'Options:',
|
||||
' --check Fail if adapter records or docs are out of sync',
|
||||
' --format <text|json|markdown>',
|
||||
' --root <path> Repository root, defaults to cwd',
|
||||
' -h, --help Show this help',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
function buildPayload(root) {
|
||||
const recordErrors = validateAdapterRecords();
|
||||
const documentationErrors = validateDocumentation({ repoRoot: root });
|
||||
|
||||
return {
|
||||
schema_version: 'ecc.harness-adapter-compliance.v1',
|
||||
generated_from: 'scripts/lib/harness-adapter-compliance.js',
|
||||
adapter_count: ADAPTER_RECORDS.length,
|
||||
valid: recordErrors.length === 0 && documentationErrors.length === 0,
|
||||
errors: [...recordErrors, ...documentationErrors],
|
||||
adapters: ADAPTER_RECORDS,
|
||||
};
|
||||
}
|
||||
|
||||
function renderText(payload) {
|
||||
const lines = [
|
||||
`Harness Adapter Compliance: ${payload.valid ? 'PASS' : 'FAIL'}`,
|
||||
`Adapters: ${payload.adapter_count}`,
|
||||
];
|
||||
|
||||
if (payload.errors.length > 0) {
|
||||
lines.push('Errors:');
|
||||
for (const error of payload.errors) {
|
||||
lines.push(`- ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
let parsed;
|
||||
|
||||
try {
|
||||
parsed = parseArgs(process.argv);
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (parsed.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildPayload(parsed.root);
|
||||
|
||||
if (parsed.format === 'json') {
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
} else if (parsed.format === 'markdown') {
|
||||
console.log(renderMarkdownTable());
|
||||
} else {
|
||||
console.log(renderText(payload));
|
||||
}
|
||||
|
||||
if (parsed.check && !payload.valid) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildPayload,
|
||||
parseArgs,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const MATRIX_BLOCK_START = '<!-- harness-adapter-compliance:matrix-start -->';
|
||||
const MATRIX_BLOCK_END = '<!-- harness-adapter-compliance:matrix-end -->';
|
||||
|
||||
const COMPLIANCE_STATES = Object.freeze({
|
||||
Native: 'ECC can install or verify the surface directly for this harness.',
|
||||
'Adapter-backed': 'ECC has a thin adapter, plugin, or package surface, but parity differs by harness.',
|
||||
'Instruction-backed': 'ECC can provide the guidance and files, but the harness does not expose the runtime hook/session surface ECC needs for enforcement.',
|
||||
'Reference-only': 'The tool is useful as a design pressure or external runtime, but ECC does not yet ship a direct installer or adapter for it.',
|
||||
});
|
||||
|
||||
const REQUIRED_FIELDS = Object.freeze([
|
||||
'id',
|
||||
'harness',
|
||||
'state',
|
||||
'supported_assets',
|
||||
'unsupported_surfaces',
|
||||
'install_or_onramp',
|
||||
'verification_commands',
|
||||
'risk_notes',
|
||||
'last_verified_at',
|
||||
'owner',
|
||||
'source_docs',
|
||||
]);
|
||||
|
||||
function freezeRecord(record) {
|
||||
return Object.freeze({
|
||||
...record,
|
||||
supported_assets: Object.freeze(record.supported_assets.slice()),
|
||||
unsupported_surfaces: Object.freeze(record.unsupported_surfaces.slice()),
|
||||
install_or_onramp: Object.freeze(record.install_or_onramp.slice()),
|
||||
verification_commands: Object.freeze(record.verification_commands.slice()),
|
||||
risk_notes: Object.freeze(record.risk_notes.slice()),
|
||||
source_docs: Object.freeze(record.source_docs.slice()),
|
||||
});
|
||||
}
|
||||
|
||||
const ADAPTER_RECORDS = Object.freeze([
|
||||
{
|
||||
id: 'claude-code',
|
||||
harness: 'Claude Code',
|
||||
state: 'Native',
|
||||
supported_assets: [
|
||||
'Claude plugin assets',
|
||||
'skills',
|
||||
'commands',
|
||||
'hooks',
|
||||
'MCP config',
|
||||
'local rules',
|
||||
'statusline-oriented workflows',
|
||||
],
|
||||
unsupported_surfaces: ['Claude-native hooks do not imply parity in other harnesses'],
|
||||
install_or_onramp: [
|
||||
'`./install.sh --profile minimal --target claude`',
|
||||
'Claude plugin install',
|
||||
],
|
||||
verification_commands: [
|
||||
'`npm run harness:audit -- --format json`',
|
||||
'`node scripts/session-inspect.js --list-adapters`',
|
||||
],
|
||||
risk_notes: ['Avoid loading every skill by default; keep hooks opt-in and inspectable.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'.claude-plugin/plugin.json',
|
||||
'docs/architecture/cross-harness.md',
|
||||
'scripts/lib/install-targets/claude-home.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'codex',
|
||||
harness: 'Codex',
|
||||
state: 'Instruction-backed',
|
||||
supported_assets: [
|
||||
'`AGENTS.md`',
|
||||
'Codex plugin metadata',
|
||||
'skills',
|
||||
'MCP reference config',
|
||||
'command patterns',
|
||||
],
|
||||
unsupported_surfaces: ['Native hook enforcement and Claude slash-command semantics are not equivalent'],
|
||||
install_or_onramp: [
|
||||
'`./install.sh --profile minimal --target codex`',
|
||||
'repo-local `AGENTS.md` review',
|
||||
],
|
||||
verification_commands: ['`npm run harness:audit -- --format json`'],
|
||||
risk_notes: ['Treat hooks as policy text unless a native Codex hook surface exists.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'.codex-plugin/plugin.json',
|
||||
'AGENTS.md',
|
||||
'scripts/lib/install-targets/codex-home.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
harness: 'OpenCode',
|
||||
state: 'Adapter-backed',
|
||||
supported_assets: [
|
||||
'OpenCode package/plugin metadata',
|
||||
'shared skills',
|
||||
'MCP config',
|
||||
'event adapter patterns',
|
||||
],
|
||||
unsupported_surfaces: ['Event names, plugin packaging, and command dispatch differ from Claude Code'],
|
||||
install_or_onramp: ['OpenCode package or plugin surface from this repo'],
|
||||
verification_commands: [
|
||||
'`node tests/scripts/build-opencode.test.js`',
|
||||
'`npm run harness:audit -- --format json`',
|
||||
],
|
||||
risk_notes: ['Keep hook logic in shared scripts and adapt only event shape at the edge.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'.opencode/package.json',
|
||||
'.opencode/plugins/ecc-hooks.ts',
|
||||
'scripts/build-opencode.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cursor',
|
||||
harness: 'Cursor',
|
||||
state: 'Adapter-backed',
|
||||
supported_assets: [
|
||||
'Cursor rules',
|
||||
'project-local skills',
|
||||
'hook adapter',
|
||||
'shared scripts',
|
||||
],
|
||||
unsupported_surfaces: ['Cursor hook events and rule loading differ from Claude Code'],
|
||||
install_or_onramp: ['`./install.sh --profile minimal --target cursor`'],
|
||||
verification_commands: [
|
||||
'`node tests/lib/install-targets.test.js`',
|
||||
'`npm run harness:audit -- --format json`',
|
||||
],
|
||||
risk_notes: ['Cursor adapters must preserve existing project rules and avoid silent overwrite.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'.cursor/',
|
||||
'scripts/lib/install-targets/cursor-project.js',
|
||||
'tests/lib/install-targets.test.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'gemini',
|
||||
harness: 'Gemini',
|
||||
state: 'Instruction-backed',
|
||||
supported_assets: [
|
||||
'Gemini project-local instructions',
|
||||
'shared skills',
|
||||
'rules',
|
||||
'compatibility docs',
|
||||
],
|
||||
unsupported_surfaces: ['No full ECC hook parity; ecosystem ports must document drift from upstream ECC'],
|
||||
install_or_onramp: ['`./install.sh --profile minimal --target gemini`'],
|
||||
verification_commands: ['`node tests/lib/install-targets.test.js`'],
|
||||
risk_notes: ['Treat Gemini ports as ecosystem adapters until validated end to end inside Gemini CLI.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'.gemini/',
|
||||
'scripts/lib/install-targets/gemini-project.js',
|
||||
'tests/lib/install-targets.test.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'zed-adjacent',
|
||||
harness: 'Zed-adjacent workflows',
|
||||
state: 'Instruction-backed',
|
||||
supported_assets: [
|
||||
'shared skills',
|
||||
'`AGENTS.md` style project instructions',
|
||||
'verification loops',
|
||||
],
|
||||
unsupported_surfaces: ['Zed agent surfaces vary; no first-party ECC installer is shipped today'],
|
||||
install_or_onramp: ['Manual copy from shared ECC sources until adapter requirements settle'],
|
||||
verification_commands: ['`npm run harness:audit -- --format json`'],
|
||||
risk_notes: ['Do not claim native Zed support before a real adapter and verification path exist.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'AGENTS.md',
|
||||
'docs/architecture/cross-harness.md',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dmux',
|
||||
harness: 'dmux',
|
||||
state: 'Adapter-backed',
|
||||
supported_assets: [
|
||||
'session snapshots',
|
||||
'tmux/worktree orchestration status',
|
||||
'handoff exports',
|
||||
],
|
||||
unsupported_surfaces: ['dmux is an orchestration runtime, not an install target for skills/rules'],
|
||||
install_or_onramp: [
|
||||
'`node scripts/session-inspect.js --list-adapters`',
|
||||
'dmux session target inspection',
|
||||
],
|
||||
verification_commands: ['`node tests/lib/session-adapters.test.js`'],
|
||||
risk_notes: ['Treat dmux events as session/runtime signals, not as a replacement for repo validation.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'scripts/lib/session-adapters/dmux-tmux.js',
|
||||
'scripts/orchestration-status.js',
|
||||
'tests/lib/session-adapters.test.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'orca',
|
||||
harness: 'Orca',
|
||||
state: 'Reference-only',
|
||||
supported_assets: [
|
||||
'worktree lifecycle',
|
||||
'review state',
|
||||
'notification',
|
||||
'provider-identity design pressure',
|
||||
],
|
||||
unsupported_surfaces: ['No ECC installer or direct adapter today'],
|
||||
install_or_onramp: ['Use as a comparison target for worktree/session state requirements'],
|
||||
verification_commands: ['`npm run observability:ready`'],
|
||||
risk_notes: ['Do not import product-specific assumptions; convert lessons into ECC event fields.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: ['docs/architecture/cross-harness.md'],
|
||||
},
|
||||
{
|
||||
id: 'superset',
|
||||
harness: 'Superset',
|
||||
state: 'Reference-only',
|
||||
supported_assets: [
|
||||
'workspace presets',
|
||||
'parallel-agent review loops',
|
||||
'worktree isolation design pressure',
|
||||
],
|
||||
unsupported_surfaces: ['No ECC installer or direct adapter today'],
|
||||
install_or_onramp: ['Use as a comparison target for workspace preset taxonomy'],
|
||||
verification_commands: ['`npm run observability:ready`'],
|
||||
risk_notes: ['Keep ECC portable; do not require a desktop workspace to get basic value.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: ['docs/architecture/cross-harness.md'],
|
||||
},
|
||||
{
|
||||
id: 'ghast',
|
||||
harness: 'Ghast',
|
||||
state: 'Reference-only',
|
||||
supported_assets: [
|
||||
'terminal-native pane grouping',
|
||||
'cwd grouping',
|
||||
'search',
|
||||
'notifications',
|
||||
],
|
||||
unsupported_surfaces: ['No ECC installer or direct adapter today'],
|
||||
install_or_onramp: ['Use as a comparison target for terminal-first session grouping'],
|
||||
verification_commands: ['`node scripts/session-inspect.js --list-adapters`'],
|
||||
risk_notes: ['Preserve terminal ergonomics before adding visual UI assumptions.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: ['docs/architecture/cross-harness.md'],
|
||||
},
|
||||
{
|
||||
id: 'terminal-only',
|
||||
harness: 'Terminal-only',
|
||||
state: 'Native',
|
||||
supported_assets: [
|
||||
'skills',
|
||||
'rules',
|
||||
'commands',
|
||||
'scripts',
|
||||
'harness audit',
|
||||
'observability readiness',
|
||||
'handoffs',
|
||||
],
|
||||
unsupported_surfaces: ['No external UI, no automatic session control unless scripts are run explicitly'],
|
||||
install_or_onramp: [
|
||||
'Clone repo',
|
||||
'run commands directly',
|
||||
'use minimal profile for project installs',
|
||||
],
|
||||
verification_commands: [
|
||||
'`npm run harness:audit -- --format json`',
|
||||
'`npm run observability:ready`',
|
||||
],
|
||||
risk_notes: ['This is the fallback contract; every higher-level adapter should degrade to it.'],
|
||||
last_verified_at: '2026-05-12',
|
||||
owner: 'ECC maintainers',
|
||||
source_docs: [
|
||||
'scripts/harness-audit.js',
|
||||
'scripts/observability-readiness.js',
|
||||
'docs/architecture/observability-readiness.md',
|
||||
],
|
||||
},
|
||||
].map(freezeRecord));
|
||||
|
||||
function toTextList(value) {
|
||||
return Array.isArray(value) ? value.join('; ') : String(value || '');
|
||||
}
|
||||
|
||||
function escapeMarkdownCell(value) {
|
||||
return toTextList(value).replace(/\|/g, '\\|').trim();
|
||||
}
|
||||
|
||||
function renderMarkdownTable(records = ADAPTER_RECORDS) {
|
||||
const lines = [
|
||||
'| Harness or runtime | State | Supported assets | Unsupported or different surfaces | Install or onramp | Verification command | Risk notes |',
|
||||
'| --- | --- | --- | --- | --- | --- | --- |',
|
||||
];
|
||||
|
||||
for (const record of records) {
|
||||
lines.push([
|
||||
record.harness,
|
||||
record.state,
|
||||
record.supported_assets,
|
||||
record.unsupported_surfaces,
|
||||
record.install_or_onramp,
|
||||
record.verification_commands,
|
||||
record.risk_notes,
|
||||
].map(escapeMarkdownCell).join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderStateTable() {
|
||||
const lines = [
|
||||
'| State | Meaning |',
|
||||
'| --- | --- |',
|
||||
];
|
||||
|
||||
for (const [state, meaning] of Object.entries(COMPLIANCE_STATES)) {
|
||||
lines.push(`| ${escapeMarkdownCell(state)} | ${escapeMarkdownCell(meaning)} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function validateAdapterRecords(records = ADAPTER_RECORDS) {
|
||||
const errors = [];
|
||||
const ids = new Set();
|
||||
|
||||
records.forEach((record, index) => {
|
||||
const label = record?.id || `record[${index}]`;
|
||||
|
||||
for (const field of REQUIRED_FIELDS) {
|
||||
if (!Object.prototype.hasOwnProperty.call(record, field)) {
|
||||
errors.push(`${label}: missing required field ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof record.id !== 'string' || !/^[a-z0-9-]+$/.test(record.id)) {
|
||||
errors.push(`${label}: id must be a lowercase slug`);
|
||||
} else if (ids.has(record.id)) {
|
||||
errors.push(`${label}: duplicate id`);
|
||||
} else {
|
||||
ids.add(record.id);
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(COMPLIANCE_STATES, record.state)) {
|
||||
errors.push(`${label}: unknown state ${record.state}`);
|
||||
}
|
||||
|
||||
for (const field of [
|
||||
'supported_assets',
|
||||
'unsupported_surfaces',
|
||||
'install_or_onramp',
|
||||
'verification_commands',
|
||||
'risk_notes',
|
||||
'source_docs',
|
||||
]) {
|
||||
if (!Array.isArray(record[field]) || record[field].length === 0) {
|
||||
errors.push(`${label}: ${field} must be a non-empty array`);
|
||||
continue;
|
||||
}
|
||||
|
||||
record[field].forEach((value, valueIndex) => {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
errors.push(`${label}: ${field}[${valueIndex}] must be a non-empty string`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof record.harness !== 'string' || !record.harness.trim()) {
|
||||
errors.push(`${label}: harness must be a non-empty string`);
|
||||
}
|
||||
|
||||
if (typeof record.owner !== 'string' || !record.owner.trim()) {
|
||||
errors.push(`${label}: owner must be a non-empty string`);
|
||||
}
|
||||
|
||||
if (typeof record.last_verified_at !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(record.last_verified_at)) {
|
||||
errors.push(`${label}: last_verified_at must be YYYY-MM-DD`);
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function extractMatrixBlock(markdown) {
|
||||
const normalized = String(markdown).replace(/\r\n/g, '\n');
|
||||
const start = normalized.indexOf(MATRIX_BLOCK_START);
|
||||
const end = normalized.indexOf(MATRIX_BLOCK_END);
|
||||
|
||||
if (start < 0 || end < 0 || end <= start) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized.slice(start + MATRIX_BLOCK_START.length, end).trim();
|
||||
}
|
||||
|
||||
function validateDocumentation(options = {}) {
|
||||
const repoRoot = options.repoRoot || path.resolve(__dirname, '..', '..');
|
||||
const docPath = options.docPath || path.join(repoRoot, 'docs', 'architecture', 'harness-adapter-compliance.md');
|
||||
const errors = [];
|
||||
const source = fs.readFileSync(docPath, 'utf8');
|
||||
const actual = extractMatrixBlock(source);
|
||||
const expected = renderMarkdownTable();
|
||||
|
||||
if (actual === null) {
|
||||
errors.push(`missing matrix block markers in ${path.relative(repoRoot, docPath)}`);
|
||||
} else if (actual !== expected) {
|
||||
errors.push(`matrix block in ${path.relative(repoRoot, docPath)} is not generated from adapter records`);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ADAPTER_RECORDS,
|
||||
COMPLIANCE_STATES,
|
||||
MATRIX_BLOCK_END,
|
||||
MATRIX_BLOCK_START,
|
||||
REQUIRED_FIELDS,
|
||||
extractMatrixBlock,
|
||||
renderMarkdownTable,
|
||||
renderStateTable,
|
||||
validateAdapterRecords,
|
||||
validateDocumentation,
|
||||
};
|
||||
@@ -87,7 +87,7 @@ There are 7 selectable category groups below. The detailed confirmation lists th
|
||||
```
|
||||
Question: "Which skill categories do you want to install?"
|
||||
Options:
|
||||
- "Framework & Language" — "Django, Laravel, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
|
||||
- "Framework & Language" — "Django, Laravel, Spring Boot, Quarkus, Go, Python, Java, Frontend, Backend patterns"
|
||||
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
|
||||
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
|
||||
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
|
||||
@@ -101,7 +101,7 @@ Options:
|
||||
|
||||
For each selected category, print the full list of skills below and ask the user to confirm or deselect specific ones. If the list exceeds 4 items, print the list as text and use `AskUserQuestion` with an "Install all listed" option plus "Other" for the user to paste specific names.
|
||||
|
||||
**Category: Framework & Language (21 skills)**
|
||||
**Category: Framework & Language (25 skills)**
|
||||
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
@@ -119,9 +119,13 @@ For each selected category, print the full list of skills below and ask the user
|
||||
| `frontend-slides` | Zero-dependency HTML presentations, style previews, and PPTX-to-web conversion |
|
||||
| `golang-patterns` | Idiomatic Go patterns, conventions for robust Go applications |
|
||||
| `golang-testing` | Go testing: table-driven tests, subtests, benchmarks, fuzzing |
|
||||
| `java-coding-standards` | Java coding standards for Spring Boot: naming, immutability, Optional, streams |
|
||||
| `java-coding-standards` | Java coding standards for Spring Boot and Quarkus: naming, immutability, Optional, streams, CDI |
|
||||
| `python-patterns` | Pythonic idioms, PEP 8, type hints, best practices |
|
||||
| `python-testing` | Python testing with pytest, TDD, fixtures, mocking, parametrization |
|
||||
| `quarkus-patterns` | Quarkus architecture, Camel messaging, CDI services, Panache data access |
|
||||
| `quarkus-security` | Quarkus security: JWT/OIDC, RBAC, input validation, secrets management |
|
||||
| `quarkus-tdd` | Quarkus TDD with JUnit 5, Mockito, REST Assured, Camel testing |
|
||||
| `quarkus-verification` | Quarkus verification: build, static analysis, tests, native compilation |
|
||||
| `springboot-patterns` | Spring Boot architecture, REST API, layered services, caching, async |
|
||||
| `springboot-security` | Spring Security: authn/authz, validation, CSRF, secrets, rate limiting |
|
||||
| `springboot-tdd` | Spring Boot TDD with JUnit 5, Mockito, MockMvc, Testcontainers |
|
||||
@@ -275,6 +279,7 @@ grep -rn "skills/" $TARGET/skills/
|
||||
Some skills reference others. Verify these dependencies:
|
||||
- `django-tdd` may reference `django-patterns`
|
||||
- `laravel-tdd` may reference `laravel-patterns`
|
||||
- `quarkus-tdd` may reference `quarkus-patterns`
|
||||
- `springboot-tdd` may reference `springboot-patterns`
|
||||
- `continuous-learning-v2` references `~/.claude/homunculus/` directory
|
||||
- `python-testing` may reference `python-patterns`
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
---
|
||||
name: java-coding-standards
|
||||
description: "Java coding standards for Spring Boot services: naming, immutability, Optional usage, streams, exceptions, generics, and project layout."
|
||||
description: "Java coding standards for Spring Boot and Quarkus services: naming, immutability, Optional usage, streams, exceptions, generics, CDI, reactive patterns, and project layout. Automatically applies framework-specific conventions."
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Java Coding Standards
|
||||
|
||||
Standards for readable, maintainable Java (17+) code in Spring Boot services.
|
||||
Standards for readable, maintainable Java (17+) code in Spring Boot and Quarkus services.
|
||||
|
||||
## When to Activate
|
||||
## When to Use
|
||||
|
||||
- Writing or reviewing Java code in Spring Boot projects
|
||||
- Writing or reviewing Java code in Spring Boot or Quarkus projects
|
||||
- Enforcing naming, immutability, or exception handling conventions
|
||||
- Working with records, sealed classes, or pattern matching (Java 17+)
|
||||
- Reviewing use of Optional, streams, or generics
|
||||
- Structuring packages and project layout
|
||||
- **[QUARKUS]**: Working with CDI scopes, Panache entities, or reactive pipelines
|
||||
|
||||
## How It Works
|
||||
|
||||
### Framework Detection
|
||||
|
||||
Before applying standards, determine the framework from the build file:
|
||||
|
||||
- Build file contains `quarkus` → apply **[QUARKUS]** conventions
|
||||
- Build file contains `spring-boot` → apply **[SPRING]** conventions
|
||||
- Neither detected → apply shared conventions only
|
||||
|
||||
## Core Principles
|
||||
|
||||
@@ -22,6 +33,13 @@ Standards for readable, maintainable Java (17+) code in Spring Boot services.
|
||||
- Immutable by default; minimize shared mutable state
|
||||
- Fail fast with meaningful exceptions
|
||||
- Consistent naming and package structure
|
||||
- **[QUARKUS]**: Favor build-time over runtime processing; avoid runtime reflection where possible
|
||||
|
||||
## Examples
|
||||
|
||||
The sections below show concrete Spring Boot, Quarkus, and shared Java examples
|
||||
for naming, immutability, dependency injection, reactive code, exceptions,
|
||||
project layout, logging, configuration, and tests.
|
||||
|
||||
## Naming
|
||||
|
||||
@@ -36,6 +54,12 @@ public Market findBySlug(String slug) {}
|
||||
|
||||
// PASS: Constants: UPPER_SNAKE_CASE
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
|
||||
// PASS: [QUARKUS] JAX-RS resources named as *Resource, not *Controller
|
||||
public class MarketResource {}
|
||||
|
||||
// PASS: [SPRING] REST controllers named as *Controller
|
||||
public class MarketController {}
|
||||
```
|
||||
|
||||
## Immutability
|
||||
@@ -49,14 +73,33 @@ public class Market {
|
||||
private final String name;
|
||||
// getters only, no setters
|
||||
}
|
||||
|
||||
// PASS: [QUARKUS] Panache active-record entities use public fields (Quarkus convention)
|
||||
@Entity
|
||||
public class Market extends PanacheEntity {
|
||||
public String name;
|
||||
public MarketStatus status;
|
||||
// Panache generates accessors at build time; public fields are idiomatic here
|
||||
}
|
||||
|
||||
// PASS: [QUARKUS] Panache MongoDB entities
|
||||
@MongoEntity(collection = "markets")
|
||||
public class Market extends PanacheMongoEntity {
|
||||
public String name;
|
||||
public MarketStatus status;
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Usage
|
||||
|
||||
```java
|
||||
// PASS: Return Optional from find* methods
|
||||
// [SPRING]
|
||||
Optional<Market> market = marketRepository.findBySlug(slug);
|
||||
|
||||
// [QUARKUS] Panache
|
||||
Optional<Market> market = Market.find("slug", slug).firstResultOptional();
|
||||
|
||||
// PASS: Map/flatMap instead of get()
|
||||
return market
|
||||
.map(MarketResponse::from)
|
||||
@@ -75,6 +118,77 @@ List<String> names = markets.stream()
|
||||
// FAIL: Avoid complex nested streams; prefer loops for clarity
|
||||
```
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
```java
|
||||
// PASS: [SPRING] Constructor injection (preferred over @Autowired on fields)
|
||||
@Service
|
||||
public class MarketService {
|
||||
private final MarketRepository marketRepository;
|
||||
|
||||
public MarketService(MarketRepository marketRepository) {
|
||||
this.marketRepository = marketRepository;
|
||||
}
|
||||
}
|
||||
|
||||
// PASS: [QUARKUS] Constructor injection
|
||||
@ApplicationScoped
|
||||
public class MarketService {
|
||||
private final MarketRepository marketRepository;
|
||||
|
||||
@Inject
|
||||
public MarketService(MarketRepository marketRepository) {
|
||||
this.marketRepository = marketRepository;
|
||||
}
|
||||
}
|
||||
|
||||
// PASS: [QUARKUS] Package-private field injection (acceptable in Quarkus — avoids proxy issues)
|
||||
@ApplicationScoped
|
||||
public class MarketService {
|
||||
@Inject
|
||||
MarketRepository marketRepository;
|
||||
}
|
||||
|
||||
// FAIL: [SPRING] Field injection with @Autowired
|
||||
@Autowired
|
||||
private MarketRepository marketRepository; // use constructor injection
|
||||
|
||||
// FAIL: [QUARKUS] @Singleton when interception or lazy init is needed
|
||||
@Singleton // non-proxyable — use @ApplicationScoped instead
|
||||
public class MarketService {}
|
||||
```
|
||||
|
||||
## Reactive Patterns [QUARKUS]
|
||||
|
||||
```java
|
||||
// PASS: Return Uni/Multi from reactive endpoints
|
||||
@GET
|
||||
@Path("/{slug}")
|
||||
public Uni<Market> findBySlug(@PathParam("slug") String slug) {
|
||||
return Market.find("slug", slug)
|
||||
.<Market>firstResult()
|
||||
.onItem().ifNull().failWith(() -> new MarketNotFoundException(slug));
|
||||
}
|
||||
|
||||
// PASS: Non-blocking pipeline composition
|
||||
public Uni<OrderConfirmation> placeOrder(OrderRequest req) {
|
||||
return validateOrder(req)
|
||||
.chain(valid -> persistOrder(valid))
|
||||
.chain(order -> notifyFulfillment(order));
|
||||
}
|
||||
|
||||
// FAIL: Blocking call inside a Uni/Multi pipeline
|
||||
public Uni<Market> find(String slug) {
|
||||
Market m = Market.find("slug", slug).firstResult(); // BLOCKING — breaks event loop
|
||||
return Uni.createFrom().item(m);
|
||||
}
|
||||
|
||||
// FAIL: Subscribing more than once to a shared Uni
|
||||
Uni<Market> shared = fetchMarket(slug);
|
||||
shared.subscribe().with(m -> log(m));
|
||||
shared.subscribe().with(m -> cache(m)); // double subscribe — use Uni.memoize()
|
||||
```
|
||||
|
||||
## Exceptions
|
||||
|
||||
- Use unchecked exceptions for domain errors; wrap technical exceptions with context
|
||||
@@ -85,6 +199,34 @@ List<String> names = markets.stream()
|
||||
throw new MarketNotFoundException(slug);
|
||||
```
|
||||
|
||||
### Centralised Exception Handling
|
||||
|
||||
```java
|
||||
// [SPRING]
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MarketNotFoundException.class)
|
||||
public ResponseEntity<ErrorResponse> handle(MarketNotFoundException ex) {
|
||||
return ResponseEntity.status(404).body(ErrorResponse.from(ex));
|
||||
}
|
||||
}
|
||||
|
||||
// [QUARKUS] Option A: ExceptionMapper
|
||||
@Provider
|
||||
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
|
||||
@Override
|
||||
public Response toResponse(MarketNotFoundException ex) {
|
||||
return Response.status(404).entity(ErrorResponse.from(ex)).build();
|
||||
}
|
||||
}
|
||||
|
||||
// [QUARKUS] Option B: @ServerExceptionMapper (RESTEasy Reactive)
|
||||
@ServerExceptionMapper
|
||||
public RestResponse<ErrorResponse> handle(MarketNotFoundException ex) {
|
||||
return RestResponse.status(Status.NOT_FOUND, ErrorResponse.from(ex));
|
||||
}
|
||||
```
|
||||
|
||||
## Generics and Type Safety
|
||||
|
||||
- Avoid raw types; declare generic parameters
|
||||
@@ -94,7 +236,9 @@ throw new MarketNotFoundException(slug);
|
||||
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
|
||||
```
|
||||
|
||||
## Project Structure (Maven/Gradle)
|
||||
## Project Structure
|
||||
|
||||
### [SPRING] Maven/Gradle
|
||||
|
||||
```
|
||||
src/main/java/com/example/app/
|
||||
@@ -110,6 +254,24 @@ src/main/resources/
|
||||
src/test/java/... (mirrors main)
|
||||
```
|
||||
|
||||
### [QUARKUS] Maven/Gradle
|
||||
|
||||
```
|
||||
src/main/java/com/example/app/
|
||||
config/ # @ConfigMapping, @ConfigProperty beans, Producers
|
||||
resource/ # JAX-RS resources (not "controller")
|
||||
service/
|
||||
repository/ # PanacheRepository implementations (if not using active record)
|
||||
domain/ # JPA/Panache entities, MongoDB entities
|
||||
dto/
|
||||
util/
|
||||
mapper/ # MapStruct mappers (if used)
|
||||
src/main/resources/
|
||||
application.properties # Quarkus convention (YAML supported with quarkus-config-yaml)
|
||||
import.sql # Hibernate auto-import for dev/test
|
||||
src/test/java/... (mirrors main)
|
||||
```
|
||||
|
||||
## Formatting and Style
|
||||
|
||||
- Use 2 or 4 spaces consistently (project standard)
|
||||
@@ -124,24 +286,98 @@ src/test/java/... (mirrors main)
|
||||
- Magic numbers → named constants
|
||||
- Static mutable state → prefer dependency injection
|
||||
- Silent catch blocks → log and act or rethrow
|
||||
- **[QUARKUS]**: `@Singleton` where `@ApplicationScoped` is intended — breaks proxying and interception
|
||||
- **[QUARKUS]**: Mixing `quarkus-resteasy-reactive` and `quarkus-resteasy` (classic) — pick one stack
|
||||
- **[QUARKUS]**: Panache active-record + repository pattern in the same bounded context — pick one
|
||||
|
||||
## Logging
|
||||
|
||||
```java
|
||||
// [SPRING] SLF4J
|
||||
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
|
||||
log.info("fetch_market slug={}", slug);
|
||||
log.error("failed_fetch_market slug={}", slug, ex);
|
||||
|
||||
// [QUARKUS] JBoss Logging (default, zero-cost at build time)
|
||||
private static final Logger log = Logger.getLogger(MarketService.class);
|
||||
log.infof("fetch_market slug=%s", slug);
|
||||
log.errorf(ex, "failed_fetch_market slug=%s", slug);
|
||||
|
||||
// [QUARKUS] Alternative: simplified logging with @Inject
|
||||
@Inject
|
||||
Logger log; // CDI-injected, scoped to declaring class
|
||||
```
|
||||
|
||||
## Null Handling
|
||||
|
||||
- Accept `@Nullable` only when unavoidable; otherwise use `@NonNull`
|
||||
- Use Bean Validation (`@NotNull`, `@NotBlank`) on inputs
|
||||
- **[QUARKUS]**: Apply `@Valid` on `@BeanParam`, `@RestForm`, and request body parameters
|
||||
|
||||
## Configuration
|
||||
|
||||
```java
|
||||
// [SPRING] @ConfigurationProperties
|
||||
@ConfigurationProperties(prefix = "market")
|
||||
public record MarketProperties(int maxPageSize, Duration cacheTtl) {}
|
||||
|
||||
// [QUARKUS] @ConfigMapping (type-safe, build-time validated)
|
||||
@ConfigMapping(prefix = "market")
|
||||
public interface MarketConfig {
|
||||
int maxPageSize();
|
||||
Duration cacheTtl();
|
||||
}
|
||||
|
||||
// [QUARKUS] Simple values with @ConfigProperty
|
||||
@ConfigProperty(name = "market.max-page-size", defaultValue = "100")
|
||||
int maxPageSize;
|
||||
```
|
||||
|
||||
## Testing Expectations
|
||||
|
||||
### Shared
|
||||
- JUnit 5 + AssertJ for fluent assertions
|
||||
- Mockito for mocking; avoid partial mocks where possible
|
||||
- Favor deterministic tests; no hidden sleeps
|
||||
|
||||
### [SPRING]
|
||||
- `@WebMvcTest` for controller slices, `@DataJpaTest` for repository slices
|
||||
- `@SpringBootTest` reserved for full integration tests
|
||||
- `@MockBean` for replacing beans in Spring context
|
||||
|
||||
### [QUARKUS]
|
||||
- Plain JUnit 5 + Mockito for unit tests (no `@QuarkusTest`)
|
||||
- `@QuarkusTest` reserved for CDI integration tests
|
||||
- `@InjectMock` for replacing CDI beans in integration tests
|
||||
- Dev Services for database/Kafka/Redis — avoid manual Testcontainers setup when Dev Services suffice
|
||||
- `@QuarkusTestResource` for custom external service lifecycle
|
||||
|
||||
```java
|
||||
// [SPRING] Controller test
|
||||
@WebMvcTest(MarketController.class)
|
||||
class MarketControllerTest {
|
||||
@Autowired MockMvc mockMvc;
|
||||
@MockBean MarketService marketService;
|
||||
}
|
||||
|
||||
// [QUARKUS] Integration test
|
||||
@QuarkusTest
|
||||
class MarketResourceTest {
|
||||
@InjectMock
|
||||
MarketService marketService;
|
||||
|
||||
@Test
|
||||
void should_return_404_when_market_not_found() {
|
||||
given().when().get("/markets/unknown").then().statusCode(404);
|
||||
}
|
||||
}
|
||||
|
||||
// [QUARKUS] Unit test (no CDI, no @QuarkusTest)
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MarketServiceTest {
|
||||
@Mock MarketRepository marketRepository;
|
||||
@InjectMocks MarketService marketService;
|
||||
}
|
||||
```
|
||||
|
||||
**Remember**: Keep code intentional, typed, and observable. Optimize for maintainability over micro-optimizations unless proven necessary.
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
---
|
||||
name: motion-advanced
|
||||
description: Advanced motion patterns for React / Next.js — drag & drop, gestures, text animations, SVG path drawing, custom hooks, imperative sequences (useAnimate), loaders, and the full API decision tree. Requires motion-foundations.
|
||||
version: 1.0
|
||||
tags: [motion, animation, advanced, gestures, svg]
|
||||
category: frontend
|
||||
author: jeff
|
||||
---
|
||||
|
||||
# Motion Advanced
|
||||
|
||||
Complex, interactive, and physics-based animation patterns.
|
||||
Requires `motion-foundations` to be set up first.
|
||||
Use these when `motion-patterns` is not enough.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building drag-to-dismiss sheets, swipe gestures, or reorderable lists
|
||||
- Animating text word-by-word, character-by-character, or as a live counter
|
||||
- Drawing SVG paths, morphing icons, or animating circular progress
|
||||
- Writing a custom animation hook (`useScrollReveal`, magnetic button, cursor follower)
|
||||
- Sequencing multi-step animations imperatively with `useAnimate`
|
||||
- Building spinners, shimmer skeletons, pulse indicators, or loading button states
|
||||
|
||||
## Outputs
|
||||
|
||||
This skill produces:
|
||||
|
||||
- Drag interactions: draggable cards, drag-to-dismiss sheets, `Reorder.Group` lists
|
||||
- Gesture hooks: swipe detection, long press, pinch outline
|
||||
- Text animation components: word reveal, character typewriter, number counter
|
||||
- SVG animation: path draw-on, icon morph, stroke progress ring
|
||||
- Custom hooks: `useScrollReveal`, `useHoverScale`, `useNavigationDirection`, `useInViewOnce`
|
||||
- Imperative sequences via `useAnimate` with interrupt-safe `async/await`
|
||||
- Loader components: spinner, shimmer, pulse dot, progress bar, button loading state
|
||||
|
||||
## Principles
|
||||
|
||||
- Physics-based motion (`useSpring`, `springs.*`) always feels more natural than duration-based for direct manipulation.
|
||||
- `useMotionValue` + `useTransform` computes derived values without triggering re-renders.
|
||||
- `useAnimate` sequences are imperative and interrupt-safe — calling `animate()` mid-flight cancels the previous animation automatically.
|
||||
- Motion values (`useMotionValue`, `useSpring`) are SSR-safe and do not cause hydration errors.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Drag interactions must be tested on touch devices**, not just mouse. `drag` prop works on both but feel and threshold differ.
|
||||
2. **Infinite animations must pause when `document.visibilityState === "hidden"`.** Background tabs must not consume GPU/CPU.
|
||||
3. **Swipe threshold must be explicit.** Never infer intent from velocity alone; combine `offset` + `velocity` checks.
|
||||
4. **`useAnimate` scope ref must be attached to a mounted DOM element.** Calling `animate()` before mount throws silently.
|
||||
5. **Motion values must not be recreated on render.** `useMotionValue(0)` inside a component body is correct; `new MotionValue(0)` in a render is not.
|
||||
6. **All token values are imported from `motion-foundations`.** No inline numbers.
|
||||
7. **Custom hooks must handle cleanup.** Every `window.addEventListener` needs a matching `removeEventListener` in the `useEffect` return.
|
||||
8. **SVG morphing requires equal path command counts.** Paths with different command structures snap instead of interpolating.
|
||||
|
||||
## Decision Guidance
|
||||
|
||||
### Choosing the right advanced API
|
||||
|
||||
| Scenario | API |
|
||||
| ------------------------------ | -------------------------------- |
|
||||
| Drag with physics on release | `drag` + `dragTransition: springs.release` |
|
||||
| Ordered drag-to-reorder list | `Reorder.Group` + `Reorder.Item` |
|
||||
| Dismiss on drag offset | `drag="y"` + `onDragEnd` offset check |
|
||||
| Swipe left/right | `drag="x"` + `onDragEnd` offset check |
|
||||
| Long press | `useLongPress` hook |
|
||||
| Value smoothed over time | `useSpring` |
|
||||
| Value derived from another | `useTransform` |
|
||||
| Multi-step sequence | `useAnimate` with `async/await` |
|
||||
| One-shot imperative animation | `animate()` from `motion` |
|
||||
| Text entering word by word | Stagger on `inline-block` spans |
|
||||
| SVG drawing on | `pathLength` 0 → 1 |
|
||||
| SVG morph | `d` attribute tween (equal commands) |
|
||||
| Circular progress | `strokeDashoffset` tween |
|
||||
|
||||
### When to use `useSpring` vs a spring transition
|
||||
|
||||
| | `useSpring` | `transition: springs.*` |
|
||||
| -------------- | ---------------------------------------- | ----------------------- |
|
||||
| Use for | Cursor follower, pointer-tracked values | Discrete state changes |
|
||||
| Updates | Continuous, on every frame | Triggered by state change |
|
||||
| Interrupt | Smooth — physics picks up from velocity | Restarts from current value |
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### useMotionValue + useTransform
|
||||
|
||||
Reactive computation without re-renders:
|
||||
|
||||
```tsx
|
||||
const x = useMotionValue(0)
|
||||
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
|
||||
// opacity updates every frame as x changes — no setState, no re-render
|
||||
```
|
||||
|
||||
### useAnimate
|
||||
|
||||
Returns `[scope, animate]`. The scope ref must be attached to a DOM element.
|
||||
`animate()` calls are interrupt-safe — calling mid-flight cancels the previous run.
|
||||
|
||||
```tsx
|
||||
const [scope, animate] = useAnimate()
|
||||
|
||||
async function play() {
|
||||
await animate(".step-1", { opacity: 1 }, { duration: 0.3 })
|
||||
await animate(".step-2", { x: 0 }, { duration: 0.4 })
|
||||
animate(".step-3", { scale: 1 }, { duration: 0.25 }) // fire and forget
|
||||
}
|
||||
|
||||
return <div ref={scope}>...</div>
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Draggable card
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { springs, motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
<motion.div
|
||||
drag
|
||||
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
|
||||
dragElastic={0.1}
|
||||
whileDrag={{
|
||||
scale: motionTokens.scale.pop,
|
||||
boxShadow: "0 16px 40px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
dragTransition={springs.release}
|
||||
/>
|
||||
```
|
||||
|
||||
### Drag-to-dismiss sheet
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, useMotionValue, useTransform } from "motion/react"
|
||||
|
||||
export function BottomSheet({ onClose }: { onClose: () => void }) {
|
||||
const y = useMotionValue(0)
|
||||
const opacity = useTransform(y, [0, 200], [1, 0])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
drag="y"
|
||||
dragConstraints={{ top: 0 }}
|
||||
style={{ y, opacity }}
|
||||
onDragEnd={(_, info) => {
|
||||
// Rule 3: combine offset + velocity
|
||||
if (info.offset.y > 120 || info.velocity.y > 500) onClose()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Reorderable list
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { Reorder } from "motion/react"
|
||||
|
||||
export function SortableList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
return (
|
||||
<Reorder.Group axis="y" values={items} onReorder={setItems}>
|
||||
{items.map((item) => (
|
||||
<Reorder.Item key={item.id} value={item}>
|
||||
{item.label}
|
||||
</Reorder.Item>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Swipe detection
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
|
||||
const OFFSET_THRESHOLD = 50
|
||||
const VELOCITY_THRESHOLD = 300
|
||||
|
||||
<motion.div
|
||||
drag="x"
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
onDragEnd={(_, info) => {
|
||||
const swipedRight = info.offset.x > OFFSET_THRESHOLD || info.velocity.x > VELOCITY_THRESHOLD
|
||||
const swipedLeft = info.offset.x < -OFFSET_THRESHOLD || info.velocity.x < -VELOCITY_THRESHOLD
|
||||
if (swipedRight) onSwipeRight()
|
||||
if (swipedLeft) onSwipeLeft()
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Long press hook
|
||||
|
||||
```tsx
|
||||
import { useRef } from "react"
|
||||
|
||||
export function useLongPress(callback: () => void, ms = 600) {
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
return {
|
||||
onPointerDown: () => { timerRef.current = setTimeout(callback, ms) },
|
||||
onPointerUp: () => clearTimeout(timerRef.current),
|
||||
onPointerLeave: () => clearTimeout(timerRef.current),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Word-by-word reveal
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { springs } from "@/lib/motion-tokens"
|
||||
|
||||
export function AnimatedText({ text }: { text: string }) {
|
||||
return (
|
||||
<motion.p
|
||||
variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{text.split(" ").map((word, i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className="inline-block mr-1"
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 12 },
|
||||
visible: { opacity: 1, y: 0, transition: springs.gentle },
|
||||
}}
|
||||
>
|
||||
{word}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Number counter
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useRef, useEffect } from "react"
|
||||
import { animate } from "motion"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function Counter({ to }: { to: number }) {
|
||||
const nodeRef = useRef<HTMLSpanElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controls = animate(0, to, {
|
||||
duration: motionTokens.duration.crawl,
|
||||
ease: motionTokens.easing.smooth,
|
||||
onUpdate: (v) => {
|
||||
if (nodeRef.current) nodeRef.current.textContent = Math.round(v).toString()
|
||||
},
|
||||
})
|
||||
return controls.stop // Rule 7: cleanup
|
||||
}, [to])
|
||||
|
||||
return <span ref={nodeRef} />
|
||||
}
|
||||
```
|
||||
|
||||
### SVG path draw-on
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
<motion.path
|
||||
d="M 0 100 Q 50 0 100 100"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Stroke progress ring
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 40 // r=40
|
||||
|
||||
export function ProgressRing({ progress }: { progress: number }) {
|
||||
return (
|
||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="8" />
|
||||
<motion.circle
|
||||
cx="50" cy="50" r="40"
|
||||
fill="none" stroke="#6366f1" strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={CIRCUMFERENCE}
|
||||
animate={{ strokeDashoffset: CIRCUMFERENCE - (progress / 100) * CIRCUMFERENCE }}
|
||||
transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
|
||||
style={{ rotate: -90, transformOrigin: "center" }}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useScrollReveal hook
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useRef } from "react"
|
||||
import { useScroll, useTransform } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function useScrollReveal() {
|
||||
const ref = useRef(null)
|
||||
const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
|
||||
const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
|
||||
const y = useTransform(scrollYProgress, [0, 0.3], [motionTokens.distance.lg, 0])
|
||||
return { ref, style: { opacity, y } }
|
||||
}
|
||||
|
||||
// Usage
|
||||
const { ref, style } = useScrollReveal()
|
||||
<motion.section ref={ref} style={style} />
|
||||
```
|
||||
|
||||
### Cursor follower
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useEffect } from "react"
|
||||
import { motion, useMotionValue, useSpring } from "motion/react"
|
||||
import { springs } from "@/lib/motion-tokens"
|
||||
|
||||
export function CursorFollower() {
|
||||
const x = useMotionValue(-100)
|
||||
const y = useMotionValue(-100)
|
||||
const sx = useSpring(x, springs.gentle)
|
||||
const sy = useSpring(y, springs.gentle)
|
||||
|
||||
useEffect(() => {
|
||||
const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
|
||||
window.addEventListener("mousemove", move)
|
||||
return () => window.removeEventListener("mousemove", move) // Rule 7
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 w-6 h-6 rounded-full bg-indigo-500
|
||||
pointer-events-none -translate-x-1/2 -translate-y-1/2 z-50"
|
||||
style={{ x: sx, y: sy }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Shimmer skeleton
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useEffect } from "react"
|
||||
import { motion, useAnimation } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function ShimmerSkeleton({ className = "" }: { className?: string }) {
|
||||
const controls = useAnimation()
|
||||
|
||||
useEffect(() => {
|
||||
const play = () =>
|
||||
controls.start({
|
||||
x: ["-100%", "100%"],
|
||||
transition: {
|
||||
repeat: Infinity,
|
||||
duration: motionTokens.duration.crawl,
|
||||
ease: motionTokens.easing.linear,
|
||||
},
|
||||
})
|
||||
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === "hidden") controls.stop()
|
||||
else void play()
|
||||
}
|
||||
|
||||
void play()
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
return () => {
|
||||
controls.stop()
|
||||
document.removeEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
}, [controls])
|
||||
|
||||
return (
|
||||
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
|
||||
initial={{ x: "-100%" }}
|
||||
animate={controls}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Button loading state
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
export function LoadingButton({
|
||||
loading,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
loading: boolean
|
||||
label: string
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<motion.button
|
||||
onClick={onClick}
|
||||
animate={{ opacity: loading ? 0.7 : 1 }}
|
||||
whileTap={loading ? {} : { scale: motionTokens.scale.press }}
|
||||
transition={springs.snappy}
|
||||
disabled={loading}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{loading ? (
|
||||
<motion.span
|
||||
key="loading"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
transition={{ duration: motionTokens.duration.fast }}
|
||||
>
|
||||
…
|
||||
</motion.span>
|
||||
) : (
|
||||
<motion.span
|
||||
key="label"
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
transition={{ duration: motionTokens.duration.fast }}
|
||||
>
|
||||
{label}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Infinite animation with visibility pause
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useEffect } from "react"
|
||||
import { motion, useAnimation } from "motion/react"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function PulseDot() {
|
||||
const controls = useAnimation()
|
||||
|
||||
useEffect(() => {
|
||||
const pulse = () =>
|
||||
controls.start({
|
||||
scale: [1, 1.4, 1],
|
||||
opacity: [1, 0.6, 1],
|
||||
transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
|
||||
})
|
||||
|
||||
// Rule 2: pause when tab is hidden
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === "hidden") controls.stop()
|
||||
else void pulse()
|
||||
}
|
||||
|
||||
void pulse()
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
// Rule 7: stop controls and remove listeners on unmount.
|
||||
return () => {
|
||||
controls.stop()
|
||||
document.removeEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
}, [controls])
|
||||
|
||||
return <motion.span className="w-2 h-2 rounded-full bg-green-400" animate={controls} />
|
||||
}
|
||||
```
|
||||
|
||||
## End-to-End Example
|
||||
|
||||
Drag-to-dismiss sheet with shimmer content, loading state, and reduced motion
|
||||
support — combining `useMotionValue`, `useTransform`, `useSafeMotion`,
|
||||
`AnimatePresence`, and tokens from `motion-foundations`:
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import { motion, AnimatePresence, useMotionValue, useTransform } from "motion/react"
|
||||
import { springs, motionTokens } from "@/lib/motion-tokens"
|
||||
import { useSafeMotion } from "@/hooks/use-reduced-motion"
|
||||
import { ShimmerSkeleton } from "./shimmer-skeleton"
|
||||
|
||||
export function DismissibleSheet({
|
||||
isOpen,
|
||||
onClose,
|
||||
loading,
|
||||
children,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
loading: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const safe = useSafeMotion(motionTokens.distance.xl)
|
||||
const y = useMotionValue(0)
|
||||
const opacity = useTransform(y, [0, 200], [1, 0])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
key="backdrop"
|
||||
className="fixed inset-0 bg-black/40"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Sheet — drag-to-dismiss */}
|
||||
<motion.div
|
||||
key="sheet"
|
||||
className="fixed bottom-0 inset-x-0 rounded-t-2xl bg-white p-6"
|
||||
drag="y"
|
||||
dragConstraints={{ top: 0 }}
|
||||
style={{ y, opacity }}
|
||||
onDragEnd={(_, info) => {
|
||||
if (info.offset.y > 120 || info.velocity.y > 500) onClose()
|
||||
}}
|
||||
initial={safe.initial}
|
||||
animate={safe.animate}
|
||||
exit={safe.exit}
|
||||
transition={springs.gentle}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<ShimmerSkeleton className="h-4 w-3/4" />
|
||||
<ShimmerSkeleton className="h-4 w-1/2" />
|
||||
<ShimmerSkeleton className="h-20 w-full" />
|
||||
</div>
|
||||
) : children}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints / Non-Goals
|
||||
|
||||
This skill does **not** cover:
|
||||
|
||||
- Token and spring definitions → see `motion-foundations`
|
||||
- Standard UI patterns (button, modal, stagger, page transitions) → see `motion-patterns`
|
||||
- CSS-only animations or Tailwind `animate-*` without `motion/react`
|
||||
- Canvas or WebGL-based animation (Three.js, Pixi, etc.)
|
||||
- Full drag-and-drop systems with external state managers (dnd-kit, react-beautiful-dnd)
|
||||
- Game-loop or frame-by-frame animation
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-pattern | Rule violated | Fix |
|
||||
| ---------------------------------------------- | ------- | ------------------------------------------------ |
|
||||
| `drag` tested only on desktop | Rule 1 | Test on touch emulator and real device |
|
||||
| `animate={{ repeat: Infinity }}` with no pause | Rule 2 | Add `visibilitychange` listener |
|
||||
| `onDragEnd` checking only offset, not velocity | Rule 3 | Check both `info.offset` and `info.velocity` |
|
||||
| `animate(scope, ...)` before `useEffect` | Rule 4 | Call `animate()` only after mount |
|
||||
| `const x = new MotionValue(0)` in render | Rule 5 | Use `const x = useMotionValue(0)` |
|
||||
| `transition={{ duration: 1.2 }}` inline | Rule 6 | Use `motionTokens.duration.crawl` |
|
||||
| `useEffect` without cleanup | Rule 7 | Return `removeEventListener` / `controls.stop` |
|
||||
| SVG morph between paths with different commands | Rule 8 | Normalize path commands before animating |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **`motion-foundations`** — defines all tokens, springs, `useSafeMotion`, and SSR guards imported here. Must be set up before using this skill.
|
||||
- **`motion-patterns`** — handles standard UI patterns (button, modal, stagger, page transitions, scroll reveals). Use it before reaching for the advanced patterns here.
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
name: motion-foundations
|
||||
description: Motion tokens, spring presets, performance rules, device adaptation, accessibility enforcement, and SSR safety for React / Next.js using motion/react. Foundation layer — all other motion skills depend on this.
|
||||
version: 1.0
|
||||
tags: [motion, animation, performance, accessibility]
|
||||
category: frontend
|
||||
author: jeff
|
||||
---
|
||||
|
||||
# Motion Foundations
|
||||
|
||||
The base layer of the motion system. Defines every value, constraint, and
|
||||
rule that downstream skills (`motion-patterns`, `motion-advanced`) inherit.
|
||||
Load this skill before any animation work begins.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Starting any animated component from scratch
|
||||
- Setting up tokens, spring presets, or easing values
|
||||
- Implementing `prefers-reduced-motion` support
|
||||
- Debugging hydration mismatches from animation initial states
|
||||
- Evaluating whether an animation should exist at all
|
||||
|
||||
## Outputs
|
||||
|
||||
This skill produces:
|
||||
|
||||
- A shared `motionTokens` object (duration, easing, distance, scale)
|
||||
- A shared `springs` preset map (5 named configs)
|
||||
- A `shouldAnimate()` gate used by all components
|
||||
- Accessibility-compliant animation defaults via `useReducedMotion`
|
||||
- SSR-safe initial states with zero hydration warnings
|
||||
|
||||
## Principles
|
||||
|
||||
Motion must do at least one of the following or it must be removed:
|
||||
|
||||
- Guide attention
|
||||
- Communicate state
|
||||
- Preserve spatial continuity
|
||||
|
||||
Responsiveness always outranks smoothness. A 60 fps animation that causes
|
||||
input delay is worse than no animation.
|
||||
|
||||
## Rules
|
||||
|
||||
These are non-negotiable. They apply to every component in the system.
|
||||
|
||||
1. **Use `motion/react` only.** Never import from `framer-motion`. Never mix the two in the same tree.
|
||||
2. **`initial` must match server output.** If the server renders `opacity: 1`, the `initial` prop must also be `opacity: 1`. No exceptions.
|
||||
3. **Reduced motion overrides everything.** When `useReducedMotion()` returns `true` or `prefersReduced` is `true`, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback.
|
||||
4. **Never animate layout properties.** `width`, `height`, `top`, `left`, `margin`, `padding` are banned from `animate`. Use `transform` and `opacity` only.
|
||||
5. **All token values come from `motionTokens`.** Hardcoded durations and easings in component files are forbidden.
|
||||
6. **All spring configs come from the `springs` map.** Inline `stiffness`/`damping` values are forbidden.
|
||||
7. **`"use client"` is required** on every file that imports from `motion/react`.
|
||||
8. **Never read `window` or `navigator` at module level.** Always guard with `typeof window !== "undefined"`.
|
||||
|
||||
## Decision Guidance
|
||||
|
||||
### Choosing a duration
|
||||
|
||||
| Token | Use when |
|
||||
| --------- | -------------------------------------------- |
|
||||
| `instant` | Tooltip show/hide, focus ring, badge update |
|
||||
| `fast` | Button feedback, icon swap, chip toggle |
|
||||
| `normal` | Modal open, card expand, page element enter |
|
||||
| `slow` | Hero entrance, full-page transition |
|
||||
| `crawl` | Deliberate storytelling; use sparingly |
|
||||
|
||||
### Choosing a spring
|
||||
|
||||
| Preset | Use when |
|
||||
| --------- | ------------------------------------------ |
|
||||
| `snappy` | Default UI — buttons, chips, nav items |
|
||||
| `gentle` | Cards, modals, panels landing softly |
|
||||
| `bouncy` | Playful moments — empty states, onboarding |
|
||||
| `instant` | Tooltips, popovers, dropdowns |
|
||||
| `release` | Drag release — natural physics feel |
|
||||
|
||||
### When to disable animation entirely
|
||||
|
||||
Disable (make `shouldAnimate()` return `false`) when:
|
||||
|
||||
- `prefersReduced` is `true`
|
||||
- `isLowEnd` is `true` and the animation is non-essential
|
||||
- The element is off-screen and will never enter the viewport
|
||||
- The animation is purely decorative with no UX purpose
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Token system
|
||||
|
||||
```ts
|
||||
// lib/motion-tokens.ts
|
||||
export const motionTokens = {
|
||||
duration: {
|
||||
instant: 0.08,
|
||||
fast: 0.18,
|
||||
normal: 0.35,
|
||||
slow: 0.6,
|
||||
crawl: 1.0,
|
||||
},
|
||||
easing: {
|
||||
smooth: [0.22, 1, 0.36, 1],
|
||||
sharp: [0.4, 0, 0.2, 1],
|
||||
bounce: [0.34, 1.56, 0.64, 1],
|
||||
linear: [0, 0, 1, 1],
|
||||
},
|
||||
distance: {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 48,
|
||||
},
|
||||
scale: {
|
||||
subtle: 0.98,
|
||||
press: 0.95,
|
||||
pop: 1.04,
|
||||
},
|
||||
}
|
||||
|
||||
export const springs = {
|
||||
snappy: { type: "spring", stiffness: 300, damping: 30 },
|
||||
gentle: { type: "spring", stiffness: 120, damping: 14 },
|
||||
bouncy: { type: "spring", stiffness: 400, damping: 10 },
|
||||
instant: { type: "spring", stiffness: 600, damping: 35 },
|
||||
release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
|
||||
}
|
||||
```
|
||||
|
||||
### Runtime flags
|
||||
|
||||
```ts
|
||||
// lib/motion-config.ts
|
||||
export const motionConfig = {
|
||||
isLowEnd() {
|
||||
return (
|
||||
typeof navigator !== "undefined" &&
|
||||
navigator.hardwareConcurrency <= 4
|
||||
)
|
||||
},
|
||||
|
||||
prefersReduced() {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
)
|
||||
},
|
||||
|
||||
shouldAnimate({ essential = false } = {}) {
|
||||
if (this.prefersReduced()) return false
|
||||
if (!essential && this.isLowEnd()) return false
|
||||
return true
|
||||
},
|
||||
|
||||
duration() {
|
||||
return this.isLowEnd() || this.prefersReduced()
|
||||
? motionTokens.duration.instant
|
||||
: motionTokens.duration.normal
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
**Priority order (highest to lowest):**
|
||||
|
||||
1. `prefers-reduced-motion: reduce` — disables all transforms, limits opacity transitions to ≤ 0.2s
|
||||
2. Low-end device detection — reduces duration, removes non-essential animations
|
||||
3. Design preference — everything else
|
||||
|
||||
Motion must degrade gracefully. It must never disappear abruptly in a way
|
||||
that causes layout shift or confuses orientation.
|
||||
|
||||
```tsx
|
||||
// hooks/use-reduced-motion.tsx
|
||||
"use client"
|
||||
import { useReducedMotion } from "motion/react"
|
||||
|
||||
export function useSafeMotion(fullY: number = 16) {
|
||||
const reduce = useReducedMotion()
|
||||
return {
|
||||
initial: { opacity: 0, y: reduce ? 0 : fullY },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: reduce ? 0 : -fullY },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* globals.css */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-safe-transition { transition: opacity 0.15s; }
|
||||
.motion-reduce-transform { transform: none !important; }
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Tailwind -->
|
||||
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
|
||||
```
|
||||
|
||||
### SSR / hydration safety
|
||||
|
||||
**Rule: `initial` must always match what the server renders.**
|
||||
|
||||
```tsx
|
||||
// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
|
||||
|
||||
// CORRECT — use AnimatePresence or defer to client mount
|
||||
"use client"
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: mounted ? 0 : 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### End-to-end: tokens + springs + accessibility + SSR guard
|
||||
|
||||
```tsx
|
||||
// components/fade-in-card.tsx
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { motion } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
import { useSafeMotion } from "@/hooks/use-reduced-motion"
|
||||
import { motionConfig } from "@/lib/motion-config"
|
||||
|
||||
interface FadeInCardProps {
|
||||
children: React.ReactNode
|
||||
delay?: number
|
||||
}
|
||||
|
||||
export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
|
||||
// SSR guard — initial must match server output (opacity: 1)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
// Accessibility — disables transform when reduced motion is preferred
|
||||
const safeMotion = useSafeMotion(motionTokens.distance.md)
|
||||
|
||||
// Device gate — skip animation on low-end hardware
|
||||
if (!motionConfig.shouldAnimate() || !mounted) {
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={safeMotion.initial}
|
||||
animate={safeMotion.animate}
|
||||
exit={safeMotion.exit}
|
||||
transition={{
|
||||
...springs.gentle,
|
||||
delay,
|
||||
}}
|
||||
whileHover={{ scale: motionTokens.scale.pop }}
|
||||
whileTap={{ scale: motionTokens.scale.press }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints / Non-Goals
|
||||
|
||||
This skill does **not** cover:
|
||||
|
||||
- UI component patterns (button, modal, stagger) → see `motion-patterns`
|
||||
- Drag, gestures, SVG, text animations, custom hooks → see `motion-advanced`
|
||||
- CSS-only animations or Tailwind `animate-*` classes without `motion/react`
|
||||
- Third-party animation libraries (GSAP, anime.js, etc.)
|
||||
- Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-pattern | Rule violated | Fix |
|
||||
| --------------------------------------- | ------- | ------------------------------- |
|
||||
| `import { motion } from "framer-motion"` | Rule 1 | Use `motion/react` |
|
||||
| `initial={{ opacity: 0 }}` on SSR component | Rule 2 | Add mount guard |
|
||||
| Skipping `useReducedMotion` check | Rule 3 | Use `useSafeMotion` hook |
|
||||
| `animate={{ width: "100%" }}` | Rule 4 | Use `scaleX` transform instead |
|
||||
| `transition={{ duration: 0.4 }}` inline | Rule 5 | Use `motionTokens.duration.normal` |
|
||||
| `{ stiffness: 300, damping: 30 }` inline | Rule 6 | Use `springs.snappy` |
|
||||
| Missing `"use client"` directive | Rule 7 | Add to top of file |
|
||||
| `navigator.hardwareConcurrency` at module level | Rule 8 | Wrap in `typeof navigator !== "undefined"` |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **`motion-patterns`** — consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.
|
||||
- **`motion-advanced`** — consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. Adds `useAnimate` sequences and custom hooks on top of this foundation.
|
||||
@@ -0,0 +1,435 @@
|
||||
---
|
||||
name: motion-patterns
|
||||
description: Production-ready animation patterns for React / Next.js — button, modal, toast, stagger, page transitions, exit animations, scroll, and layout — built on motion-foundations tokens and springs.
|
||||
version: 1.0
|
||||
tags: [motion, animation, ui-patterns]
|
||||
category: frontend
|
||||
author: jeff
|
||||
---
|
||||
|
||||
# Motion Patterns
|
||||
|
||||
Copy-paste patterns for the most common UI animation needs.
|
||||
Every pattern here is built on `motion-foundations` tokens and springs.
|
||||
Do not define new duration or easing values here — import them.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Animating a button, card, modal, or toast notification
|
||||
- Building list entrances with stagger
|
||||
- Setting up page transitions in Next.js App Router
|
||||
- Adding entrance or exit animations to conditional content
|
||||
- Implementing scroll-reveal, scroll-linked progress, or sticky story sections
|
||||
- Building expanding cards, accordions, or shared-element transitions
|
||||
|
||||
## Outputs
|
||||
|
||||
This skill produces:
|
||||
|
||||
- Accessible, SSR-safe animation for all standard UI components
|
||||
- `AnimatePresence`-wrapped conditional renders with correct exit behavior
|
||||
- Page transition wrapper component for Next.js App Router
|
||||
- Scroll-reveal and scroll-linked patterns using `useScroll` + `useTransform`
|
||||
- Layout animation patterns (`layout`, `layoutId`) for expanding and crossfading elements
|
||||
|
||||
## Principles
|
||||
|
||||
- Every pattern imports from `motion-foundations`. No raw numbers.
|
||||
- Every conditional render is wrapped in `AnimatePresence` with a `key`.
|
||||
- Exit animations are always defined alongside enter animations — never as an afterthought.
|
||||
- `layout` is used only for small, isolated shifts. Large subtrees get explicit transforms.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always wrap conditional renders in `AnimatePresence` with a `key`** on the direct child. Without a key, exit animations never fire.
|
||||
2. **Always define `exit` when defining `initial` + `animate`.** An animation without an exit is incomplete.
|
||||
3. **Use `mode="wait"` on page transitions.** Enter must not start until exit completes.
|
||||
4. **Never use `layout` on subtrees with more than ~5 children or deeply nested DOM.** Use explicit `x`/`y` transforms instead.
|
||||
5. **Stagger interval must stay between `0.05s` and `0.10s`.** Below feels mechanical; above feels sluggish.
|
||||
6. **Modals must always include:** focus trap, Escape-key close, scroll lock, `role="dialog"`, `aria-modal="true"`.
|
||||
7. **Scroll reveals use `viewport={{ once: true }}`.** Repeating on scroll-out is distracting, not informative.
|
||||
8. **All token values are imported from `motion-foundations`.** No inline numbers.
|
||||
|
||||
## Decision Guidance
|
||||
|
||||
### Choosing the right pattern
|
||||
|
||||
| Situation | Pattern |
|
||||
| ---------------------------------------- | ---------------------- |
|
||||
| Element appears / disappears | `AnimatePresence` |
|
||||
| List of items loading in sequence | Stagger variants |
|
||||
| Navigating between routes | Page transition wrapper|
|
||||
| Element changes size in place | `layout` prop |
|
||||
| Same element moves across page contexts | `layoutId` |
|
||||
| Element enters when scrolled into view | `whileInView` |
|
||||
| Value tied to scroll position | `useScroll` + `useTransform` |
|
||||
|
||||
### When to use `mode="wait"` vs `mode="sync"`
|
||||
|
||||
| Mode | Use when |
|
||||
| ------- | --------------------------------------- |
|
||||
| `wait` | Page transitions, content swaps (one at a time) |
|
||||
| `sync` | Stacked notifications, list items (overlap is fine) |
|
||||
| `popLayout` | Items removed from a reflow list |
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### AnimatePresence contract
|
||||
|
||||
Three things must always be true:
|
||||
|
||||
1. `AnimatePresence` wraps the conditional
|
||||
2. The direct child has a `key`
|
||||
3. The child has an `exit` prop
|
||||
|
||||
Miss any one of these and the exit animation silently fails.
|
||||
|
||||
### layout vs layoutId
|
||||
|
||||
- `layout` — animates the element's own size/position change in place
|
||||
- `layoutId` — links two separate elements, crossfading between them across renders
|
||||
|
||||
Use `layout="position"` on text inside an expanding container to prevent text reflow from animating.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Button feedback
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { springs, motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: motionTokens.scale.pop }}
|
||||
whileTap={{ scale: motionTokens.scale.press }}
|
||||
transition={springs.snappy}
|
||||
/>
|
||||
```
|
||||
|
||||
### Stagger list
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
const container = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.08, // within the 0.05–0.10 rule
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const item = {
|
||||
hidden: { opacity: 0, y: motionTokens.distance.md },
|
||||
visible: { opacity: 1, y: 0, transition: springs.gentle },
|
||||
}
|
||||
|
||||
<motion.ul variants={container} initial="hidden" animate="visible">
|
||||
{items.map((i) => (
|
||||
<motion.li key={i.id} variants={item} />
|
||||
))}
|
||||
</motion.ul>
|
||||
```
|
||||
|
||||
### Modal
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
// Wrap at the call site:
|
||||
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>
|
||||
|
||||
export function Modal({ onClose }: { onClose: () => void }) {
|
||||
return (
|
||||
<>
|
||||
{/* Overlay */}
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black/50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Panel — accessibility requirements: focus trap, Escape close,
|
||||
scroll lock, role="dialog", aria-modal="true" */}
|
||||
<motion.div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: motionTokens.scale.press,
|
||||
y: motionTokens.distance.sm,
|
||||
}}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
scale: motionTokens.scale.press,
|
||||
y: motionTokens.distance.sm,
|
||||
}}
|
||||
transition={springs.gentle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Toast stack
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
<AnimatePresence mode="sync">
|
||||
{toasts.map((t) => (
|
||||
<motion.div
|
||||
key={t.id}
|
||||
layout
|
||||
initial={{
|
||||
opacity: 0,
|
||||
x: motionTokens.distance.xl,
|
||||
scale: motionTokens.scale.subtle,
|
||||
}}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: motionTokens.distance.xl,
|
||||
scale: motionTokens.scale.subtle,
|
||||
}}
|
||||
transition={springs.snappy}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
### Page transition (Next.js App Router)
|
||||
|
||||
```tsx
|
||||
// components/page-transition.tsx
|
||||
"use client"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
const variants = {
|
||||
initial: { opacity: 0, y: motionTokens.distance.sm },
|
||||
enter: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -motionTokens.distance.sm },
|
||||
}
|
||||
|
||||
export function PageTransition({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={pathname}
|
||||
variants={variants}
|
||||
initial="initial"
|
||||
animate="enter"
|
||||
exit="exit"
|
||||
transition={{
|
||||
duration: motionTokens.duration.normal,
|
||||
ease: motionTokens.easing.smooth,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Scroll reveal
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: motionTokens.distance.lg }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-80px" }} // once: true — rule 7
|
||||
transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Scroll progress bar
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { motion, useScroll } from "motion/react"
|
||||
|
||||
export function ScrollProgress() {
|
||||
const { scrollYProgress } = useScroll()
|
||||
return (
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 h-1 bg-indigo-500 origin-left w-full"
|
||||
style={{ scaleX: scrollYProgress }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Expanding card
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { springs, motionTokens } from "@/lib/motion-tokens"
|
||||
|
||||
export function ExpandingCard({ title, body }: { title: string; body: string }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<motion.div layout onClick={() => setExpanded(!expanded)} className="cursor-pointer">
|
||||
{/* layout="position" prevents text reflow from animating */}
|
||||
<motion.h2 layout="position" className="font-semibold">
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<AnimatePresence>
|
||||
{expanded && (
|
||||
<motion.p
|
||||
key="body"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: motionTokens.duration.fast }}
|
||||
>
|
||||
{body}
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Shared-element crossfade
|
||||
|
||||
```tsx
|
||||
// Source context
|
||||
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />
|
||||
|
||||
// Destination context (same layoutId — motion handles the transition)
|
||||
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />
|
||||
```
|
||||
|
||||
### Accordion
|
||||
|
||||
```tsx
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
|
||||
style={{ transformOrigin: "top", overflow: "hidden" }}
|
||||
transition={{
|
||||
duration: motionTokens.duration.normal,
|
||||
ease: motionTokens.easing.smooth,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
```
|
||||
|
||||
## End-to-End Example
|
||||
|
||||
A staggered list that enters on mount, handles conditional presence, and
|
||||
respects reduced motion — combining tokens, springs, AnimatePresence, and
|
||||
the accessibility hook from `motion-foundations`:
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { motionTokens, springs } from "@/lib/motion-tokens"
|
||||
import { useSafeMotion } from "@/hooks/use-reduced-motion"
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
|
||||
},
|
||||
}
|
||||
|
||||
function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
|
||||
const safe = useSafeMotion(motionTokens.distance.sm)
|
||||
return (
|
||||
<motion.li
|
||||
variants={{
|
||||
hidden: safe.initial,
|
||||
visible: safe.animate,
|
||||
}}
|
||||
exit={safe.exit}
|
||||
transition={springs.gentle}
|
||||
className="flex items-center justify-between p-3 rounded-lg bg-white shadow-sm"
|
||||
>
|
||||
<span>{label}</span>
|
||||
<button onClick={onRemove}>Remove</button>
|
||||
</motion.li>
|
||||
)
|
||||
}
|
||||
|
||||
export function AnimatedList({ items, onRemove }: {
|
||||
items: { id: string; label: string }[]
|
||||
onRemove: (id: string) => void
|
||||
}) {
|
||||
return (
|
||||
<motion.ul
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="space-y-2"
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{items.map((item) => (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
label={item.label}
|
||||
onRemove={() => onRemove(item.id)}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints / Non-Goals
|
||||
|
||||
This skill does **not** cover:
|
||||
|
||||
- Token and spring definitions → see `motion-foundations`
|
||||
- Drag interactions, swipe gestures, reorderable lists → see `motion-advanced`
|
||||
- Text animations (word/character reveal, counters) → see `motion-advanced`
|
||||
- SVG path drawing or morphing → see `motion-advanced`
|
||||
- Custom animation hooks → see `motion-advanced`
|
||||
- CSS-only transitions not using `motion/react`
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-pattern | Rule violated | Fix |
|
||||
| -------------------------------------------- | ------- | ------------------------------------------ |
|
||||
| `AnimatePresence` child missing `key` | Rule 1 | Add stable `key` to the direct child |
|
||||
| `initial` + `animate` without `exit` | Rule 2 | Always define all three together |
|
||||
| Page transition without `mode="wait"` | Rule 3 | Add `mode="wait"` to `AnimatePresence` |
|
||||
| `layout` on a 50-item list | Rule 4 | Use `mode="popLayout"` or explicit transforms |
|
||||
| `staggerChildren: 0.2` on a 10-item list | Rule 5 | Cap at `0.08–0.10` |
|
||||
| Modal without focus trap | Rule 6 | Add `focus-trap-react` or Radix Dialog |
|
||||
| `whileInView` without `viewport={{ once: true }}` | Rule 7 | Repeating entrances distract, not inform |
|
||||
| `transition={{ duration: 0.3 }}` inline | Rule 8 | Use `motionTokens.duration.normal` |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **`motion-foundations`** — defines all tokens, springs, the `useSafeMotion` hook, and SSR guards that every pattern here imports. Must be set up first.
|
||||
- **`motion-advanced`** — extends these patterns with drag, gestures, SVG, text, custom hooks, and imperative sequencing. Does not redefine any patterns from this skill.
|
||||
@@ -68,7 +68,7 @@ Before analyzing the prompt, detect the current project context:
|
||||
- `go.mod` → Go
|
||||
- `pyproject.toml` / `requirements.txt` → Python
|
||||
- `Cargo.toml` → Rust
|
||||
- `build.gradle` / `pom.xml` → Java / Kotlin / Spring Boot
|
||||
- `build.gradle` / `pom.xml` → Java / Kotlin (then check for `quarkus` in build file → Quarkus, or `spring-boot` → Spring Boot)
|
||||
- `Package.swift` → Swift
|
||||
- `Gemfile` → Ruby
|
||||
- `composer.json` → PHP
|
||||
@@ -134,7 +134,8 @@ Map intent + scope + tech stack (from Phase 0) to specific ECC components.
|
||||
|------------|--------------|-------|
|
||||
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
||||
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | java-reviewer |
|
||||
| Quarkus / Java | quarkus-patterns, quarkus-tdd, quarkus-security, quarkus-verification, java-coding-standards, jpa-patterns | java-reviewer |
|
||||
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
||||
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
||||
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
||||
|
||||
@@ -50,6 +50,7 @@ const expectedReleaseFiles = [
|
||||
'telegram-handoff.md',
|
||||
'demo-prompts.md',
|
||||
'quickstart.md',
|
||||
'publication-readiness.md',
|
||||
];
|
||||
|
||||
test('release candidate directory includes the public launch pack', () => {
|
||||
@@ -175,6 +176,53 @@ test('launch checklist records the ecc2 alpha version policy', () => {
|
||||
assert.ok(!launchChecklist.includes('confirm whether `ecc2/Cargo.toml` moves'));
|
||||
});
|
||||
|
||||
test('publication readiness checklist gates public release actions on evidence', () => {
|
||||
const source = read('docs/releases/2.0.0-rc.1/publication-readiness.md');
|
||||
|
||||
for (const section of [
|
||||
'## Release Identity Matrix',
|
||||
'## Publication Gates',
|
||||
'## Required Command Evidence',
|
||||
'## Do Not Publish If',
|
||||
'## Announcement Order',
|
||||
]) {
|
||||
assert.ok(source.includes(section), `publication readiness missing ${section}`);
|
||||
}
|
||||
|
||||
for (const field of [
|
||||
'Fresh check',
|
||||
'Evidence artifact',
|
||||
'Owner',
|
||||
'Status',
|
||||
'Blocker field',
|
||||
'Recorded output',
|
||||
]) {
|
||||
assert.ok(source.includes(field), `publication readiness missing ${field}`);
|
||||
}
|
||||
|
||||
for (const surface of [
|
||||
'GitHub release',
|
||||
'npm package',
|
||||
'Claude plugin',
|
||||
'Codex plugin',
|
||||
'OpenCode package',
|
||||
'ECC Tools billing reference',
|
||||
'Announcement copy',
|
||||
]) {
|
||||
assert.ok(source.includes(surface), `publication readiness missing ${surface}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('release checklist and roadmap link to publication readiness evidence gate', () => {
|
||||
const launchChecklist = read('docs/releases/2.0.0-rc.1/launch-checklist.md');
|
||||
const roadmap = read('docs/ECC-2.0-GA-ROADMAP.md');
|
||||
|
||||
assert.ok(launchChecklist.includes('publication-readiness.md'));
|
||||
assert.ok(launchChecklist.includes('fresh evidence'));
|
||||
assert.ok(roadmap.includes('docs/releases/2.0.0-rc.1/publication-readiness.md'));
|
||||
assert.ok(roadmap.includes('npm dist-tag'));
|
||||
});
|
||||
|
||||
test('localized changelogs include rc.1 and 1.10.0 release entries', () => {
|
||||
for (const relativePath of ['docs/tr/CHANGELOG.md', 'docs/zh-CN/CHANGELOG.md']) {
|
||||
const source = read(relativePath);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
ADAPTER_RECORDS,
|
||||
extractMatrixBlock,
|
||||
renderMarkdownTable,
|
||||
validateAdapterRecords,
|
||||
} = require('../../scripts/lib/harness-adapter-compliance');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const scriptPath = path.join(repoRoot, 'scripts', 'harness-adapter-compliance.js');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
console.log('\n=== Testing harness adapter compliance docs ===\n');
|
||||
|
||||
test('adapter compliance matrix covers the required harness surfaces', () => {
|
||||
const source = read('docs/architecture/harness-adapter-compliance.md');
|
||||
for (const harness of [
|
||||
'Claude Code',
|
||||
'Codex',
|
||||
'OpenCode',
|
||||
'Cursor',
|
||||
'Gemini',
|
||||
'Zed-adjacent',
|
||||
'dmux',
|
||||
'Orca',
|
||||
'Superset',
|
||||
'Ghast',
|
||||
'Terminal-only'
|
||||
]) {
|
||||
assert.ok(source.includes(harness), `Expected matrix to include ${harness}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('adapter compliance source data validates required evidence fields', () => {
|
||||
assert.deepStrictEqual(validateAdapterRecords(), []);
|
||||
|
||||
for (const record of ADAPTER_RECORDS) {
|
||||
assert.ok(record.install_or_onramp.length > 0, `${record.id} needs an install or onramp`);
|
||||
assert.ok(record.verification_commands.length > 0, `${record.id} needs verification commands`);
|
||||
assert.ok(record.risk_notes.length > 0, `${record.id} needs risk notes`);
|
||||
assert.ok(record.source_docs.length > 0, `${record.id} needs source docs`);
|
||||
}
|
||||
});
|
||||
|
||||
test('adapter compliance matrix is generated from source data', () => {
|
||||
const source = read('docs/architecture/harness-adapter-compliance.md');
|
||||
assert.strictEqual(extractMatrixBlock(source), renderMarkdownTable());
|
||||
});
|
||||
|
||||
test('adapter compliance matrix extraction tolerates Windows line endings', () => {
|
||||
const source = read('docs/architecture/harness-adapter-compliance.md')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\n/g, '\r\n');
|
||||
assert.strictEqual(extractMatrixBlock(source), renderMarkdownTable());
|
||||
});
|
||||
|
||||
test('adapter compliance matrix includes the required evidence columns', () => {
|
||||
const source = read('docs/architecture/harness-adapter-compliance.md');
|
||||
for (const heading of [
|
||||
'Supported assets',
|
||||
'Unsupported or different surfaces',
|
||||
'Install or onramp',
|
||||
'Verification command',
|
||||
'Risk notes'
|
||||
]) {
|
||||
assert.ok(source.includes(heading), `Expected matrix to include ${heading}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('scorecard onramp names the local verification commands', () => {
|
||||
const source = read('docs/architecture/harness-adapter-compliance.md');
|
||||
for (const command of [
|
||||
'npm run harness:adapters -- --check',
|
||||
'npm run harness:audit -- --format json',
|
||||
'npm run observability:ready',
|
||||
'node scripts/session-inspect.js --list-adapters',
|
||||
'node scripts/loop-status.js --json --write-dir .ecc/loop-status'
|
||||
]) {
|
||||
assert.ok(source.includes(command), `Expected onramp to include ${command}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('adapter compliance CLI check passes against the committed doc', () => {
|
||||
const output = execFileSync('node', [scriptPath, '--check'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
assert.ok(output.includes('Harness Adapter Compliance: PASS'));
|
||||
assert.ok(output.includes(`Adapters: ${ADAPTER_RECORDS.length}`));
|
||||
});
|
||||
|
||||
test('adapter compliance CLI emits machine-readable scorecard data', () => {
|
||||
const output = execFileSync('node', [scriptPath, '--format=json'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const parsed = JSON.parse(output);
|
||||
|
||||
assert.strictEqual(parsed.schema_version, 'ecc.harness-adapter-compliance.v1');
|
||||
assert.strictEqual(parsed.valid, true);
|
||||
assert.strictEqual(parsed.adapter_count, ADAPTER_RECORDS.length);
|
||||
assert.ok(parsed.adapters.some(record => record.id === 'terminal-only'));
|
||||
});
|
||||
|
||||
test('cross-harness architecture links to the adapter compliance matrix', () => {
|
||||
const source = read('docs/architecture/cross-harness.md');
|
||||
assert.ok(source.includes('harness-adapter-compliance.md'));
|
||||
});
|
||||
|
||||
test('GA roadmap records the matrix and validator as current evidence', () => {
|
||||
const source = read('docs/ECC-2.0-GA-ROADMAP.md');
|
||||
assert.ok(source.includes('docs/architecture/harness-adapter-compliance.md'));
|
||||
assert.ok(source.includes('npm run harness:adapters -- --check'));
|
||||
assert.ok(source.includes('scripts/lib/harness-adapter-compliance.js'));
|
||||
});
|
||||
|
||||
if (failed > 0) {
|
||||
console.log(`\nFailed: ${failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
@@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const legacyShimsDir = path.join(repoRoot, 'legacy-command-shims', 'commands');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
function findLegacyDocumentDirs(dir) {
|
||||
const results = [];
|
||||
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextPath = path.join(dir, entry.name);
|
||||
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.name.startsWith('_legacy-documents-')) {
|
||||
results.push(path.relative(repoRoot, nextPath));
|
||||
}
|
||||
|
||||
results.push(...findLegacyDocumentDirs(nextPath));
|
||||
}
|
||||
|
||||
return results.sort();
|
||||
}
|
||||
|
||||
console.log('\n=== Testing legacy artifact inventory ===\n');
|
||||
|
||||
test('legacy artifact inventory documents classification states', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
|
||||
for (const state of [
|
||||
'Landed',
|
||||
'Milestone-tracked',
|
||||
'Salvage branch',
|
||||
'Translator/manual review',
|
||||
'Archive/no-action',
|
||||
]) {
|
||||
assert.ok(source.includes(state), `Missing classification state ${state}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('any _legacy-documents directories are explicitly inventoried', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
const dirs = findLegacyDocumentDirs(repoRoot);
|
||||
|
||||
for (const dir of dirs) {
|
||||
assert.ok(source.includes(dir), `Missing legacy artifact inventory row for ${dir}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('workspace-level legacy repos are inventoried without personal paths', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
|
||||
for (const dir of [
|
||||
'../_legacy-documents-ecc-context-2026-04-30',
|
||||
'../_legacy-documents-ecc-everything-claude-code-2026-04-30',
|
||||
]) {
|
||||
assert.ok(source.includes(dir), `Missing workspace legacy repo ${dir}`);
|
||||
}
|
||||
|
||||
assert.ok(source.includes('Workspace-Level Legacy Repos'));
|
||||
assert.ok(!source.includes('/Users/'), 'Inventory should avoid machine-local absolute paths');
|
||||
});
|
||||
|
||||
test('workspace legacy import rules block raw private context', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
|
||||
for (const required of [
|
||||
'Do not read, print, stage, or copy `.env` files',
|
||||
'tokens',
|
||||
'OAuth secrets',
|
||||
'personal paths',
|
||||
'private operator context',
|
||||
'Do not import raw marketing drafts',
|
||||
'public-safe ideas',
|
||||
]) {
|
||||
assert.ok(source.includes(required), `Missing import guardrail: ${required}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy command shims remain classified as an opt-in archive', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
const readme = read('legacy-command-shims/README.md');
|
||||
|
||||
assert.ok(source.includes('legacy-command-shims/'));
|
||||
assert.ok(source.includes('Archive/no-action'));
|
||||
assert.ok(readme.includes('no longer loaded by the default plugin command surface'));
|
||||
assert.ok(readme.includes('short-term migration compatibility'));
|
||||
});
|
||||
|
||||
test('legacy command shim table tracks the current archive contents', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
const shims = fs.readdirSync(legacyShimsDir)
|
||||
.filter(fileName => fileName.endsWith('.md'))
|
||||
.sort();
|
||||
|
||||
assert.strictEqual(shims.length, 12);
|
||||
|
||||
for (const shim of shims) {
|
||||
assert.ok(source.includes(`\`${shim}\``), `Missing legacy shim ${shim}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('stale salvage backlog records the remaining manual-review tail', () => {
|
||||
const source = read('docs/legacy-artifact-inventory.md');
|
||||
|
||||
assert.ok(source.includes('#1687 zh-CN localization tail'));
|
||||
assert.ok(source.includes('Translator/manual review'));
|
||||
assert.ok(source.includes('#1746-#1752'));
|
||||
});
|
||||
|
||||
if (failed > 0) {
|
||||
console.log(`\nFailed: ${failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
console.log('\n=== Testing stale PR salvage ledger ===\n');
|
||||
|
||||
test('stale PR salvage ledger defines every disposition state', () => {
|
||||
const source = read('docs/stale-pr-salvage-ledger.md');
|
||||
|
||||
for (const state of [
|
||||
'Salvaged',
|
||||
'Already present',
|
||||
'Superseded',
|
||||
'Skipped',
|
||||
'Translator/manual review',
|
||||
]) {
|
||||
assert.ok(source.includes(state), `Missing salvage state ${state}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('stale PR salvage ledger preserves representative source attribution', () => {
|
||||
const source = read('docs/stale-pr-salvage-ledger.md');
|
||||
|
||||
for (const pr of [
|
||||
'#1309',
|
||||
'#1322',
|
||||
'#1326',
|
||||
'#1413',
|
||||
'#1493',
|
||||
'#1528/#1529/#1547',
|
||||
'#1674',
|
||||
'#1687',
|
||||
'#1705/#1780',
|
||||
'#1757',
|
||||
]) {
|
||||
assert.ok(source.includes(pr), `Missing source PR attribution for ${pr}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('stale PR salvage ledger records skipped junk and superseded work', () => {
|
||||
const source = read('docs/stale-pr-salvage-ledger.md');
|
||||
|
||||
for (const pr of ['#1306', '#1337', '#1341', '#1416/#1465', '#1475']) {
|
||||
assert.ok(source.includes(pr), `Missing skipped or superseded PR ${pr}`);
|
||||
}
|
||||
|
||||
assert.ok(source.includes('Accidental fork-sync PRs'));
|
||||
assert.ok(source.includes('too low-signal'));
|
||||
});
|
||||
|
||||
test('stale PR salvage ledger keeps the zh-CN tail manual-review only', () => {
|
||||
const source = read('docs/stale-pr-salvage-ledger.md');
|
||||
|
||||
assert.ok(source.includes('Only the #1687 localization tail remains'));
|
||||
assert.ok(source.includes('translator/manual review'));
|
||||
assert.ok(source.includes('Do not import stale top-level docs'));
|
||||
});
|
||||
|
||||
test('legacy inventory and roadmap link to the durable salvage ledger', () => {
|
||||
const inventory = read('docs/legacy-artifact-inventory.md');
|
||||
const roadmap = read('docs/ECC-2.0-GA-ROADMAP.md');
|
||||
|
||||
assert.ok(inventory.includes('docs/stale-pr-salvage-ledger.md'));
|
||||
assert.ok(roadmap.includes('docs/stale-pr-salvage-ledger.md'));
|
||||
assert.ok(roadmap.includes('#1687 translator/manual'));
|
||||
});
|
||||
|
||||
if (failed > 0) {
|
||||
console.log(`\nFailed: ${failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
|
||||
@@ -20,33 +20,38 @@ function cleanup(dirPath) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function writeFakePython(binDir) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const fakePythonJs = path.join(binDir, 'fake-python.js');
|
||||
fs.writeFileSync(fakePythonJs, [
|
||||
"'use strict';",
|
||||
"const fs = require('fs');",
|
||||
"const mode = process.env.FAKE_INSAITS_MODE || 'clean';",
|
||||
"if (mode === 'clean') {",
|
||||
" fs.readFileSync(0, 'utf8');",
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
"if (mode === 'echo') {",
|
||||
" process.stdout.write(fs.readFileSync(0, 'utf8'));",
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
"if (mode === 'block') {",
|
||||
" process.stdout.write('blocked by monitor\\n');",
|
||||
" process.stderr.write('monitor warning\\n');",
|
||||
" process.exit(2);",
|
||||
"}",
|
||||
"if (mode === 'error') {",
|
||||
" process.stderr.write('spawned but failed\\n');",
|
||||
" process.exit(1);",
|
||||
"}",
|
||||
].join('\n'), 'utf8');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const fakePythonJs = path.join(binDir, 'fake-python.js');
|
||||
const fakePythonCmd = path.join(binDir, 'python3.cmd');
|
||||
fs.writeFileSync(fakePythonJs, [
|
||||
"'use strict';",
|
||||
"const fs = require('fs');",
|
||||
"const mode = process.env.FAKE_INSAITS_MODE || 'clean';",
|
||||
"if (mode === 'clean') {",
|
||||
" fs.readFileSync(0, 'utf8');",
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
"if (mode === 'echo') {",
|
||||
" process.stdout.write(fs.readFileSync(0, 'utf8'));",
|
||||
" process.exit(0);",
|
||||
"}",
|
||||
"if (mode === 'block') {",
|
||||
" process.stdout.write('blocked by monitor\\n');",
|
||||
" process.stderr.write('monitor warning\\n');",
|
||||
" process.exit(2);",
|
||||
"}",
|
||||
"if (mode === 'error') {",
|
||||
" process.stderr.write('spawned but failed\\n');",
|
||||
" process.exit(1);",
|
||||
"}",
|
||||
].join('\n'), 'utf8');
|
||||
fs.writeFileSync(fakePythonCmd, [
|
||||
'@echo off',
|
||||
`"${process.execPath}" "%~dp0fake-python.js" %*`,
|
||||
@@ -57,26 +62,7 @@ function writeFakePython(binDir) {
|
||||
const fakePython = path.join(binDir, 'python3');
|
||||
fs.writeFileSync(fakePython, [
|
||||
'#!/bin/sh',
|
||||
'mode="${FAKE_INSAITS_MODE:-clean}"',
|
||||
'case "$mode" in',
|
||||
' clean)',
|
||||
' cat >/dev/null',
|
||||
' exit 0',
|
||||
' ;;',
|
||||
' echo)',
|
||||
' cat',
|
||||
' exit 0',
|
||||
' ;;',
|
||||
' block)',
|
||||
' printf "blocked by monitor\\n"',
|
||||
' printf "monitor warning\\n" >&2',
|
||||
' exit 2',
|
||||
' ;;',
|
||||
' error)',
|
||||
' printf "spawned but failed\\n" >&2',
|
||||
' exit 1',
|
||||
' ;;',
|
||||
'esac',
|
||||
`exec ${shellQuote(process.execPath)} ${shellQuote(fakePythonJs)} "$@"`,
|
||||
].join('\n'), 'utf8');
|
||||
fs.chmodSync(fakePython, 0o755);
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ async function runTests() {
|
||||
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
|
||||
ECC_MCP_CONFIG_PATH: configPath,
|
||||
ECC_MCP_HEALTH_STATE_PATH: statePath,
|
||||
ECC_MCP_HEALTH_TIMEOUT_MS: process.platform === 'win32' ? '1000' : '100'
|
||||
ECC_MCP_HEALTH_TIMEOUT_MS: '1000'
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ function buildExpectedPublishPaths(repoRoot) {
|
||||
"scripts/observability-readiness.js",
|
||||
"scripts/skill-create-output.js",
|
||||
"scripts/repair.js",
|
||||
"scripts/harness-adapter-compliance.js",
|
||||
"scripts/harness-audit.js",
|
||||
"scripts/session-inspect.js",
|
||||
"scripts/uninstall.js",
|
||||
|
||||
Reference in New Issue
Block a user