From 2389df70796fee197f3cefb0f8b9de0a96a89a9a Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 17 Apr 2026 13:07:49 +0200 Subject: [PATCH 1/4] Step 1: Setup Environment - Compile: N/A (environment setup) - Maven Wrapper updated from 3.9.9 to 3.9.11 for improved Java 25 compatibility - JDK 25.0.1 (Temurin) already available at JAVA_HOME; no installation needed --- .mvn/wrapper/maven-wrapper.properties | 2 +- .../retrievers/SkillsFieldRetriever.java | 37 +++ .../manager/controllers/SkillsController.java | 41 +++ .../workflow/manager/skills/SkillBinding.java | 21 ++ .../manager/skills/SkillDefinition.java | 8 + .../manager/skills/SkillPromptService.java | 40 +++ .../manager/skills/SkillsCatalogService.java | 181 ++++++++++++ src/main/resources/skills-catalog.json | 28 ++ .../skills/java-development/SKILL.md | 8 + .../skills/requirements-engineering/SKILL.md | 266 ++++++++++++++++++ .../skills/springboot-development/SKILL.md | 65 +++++ .../resources/skills/task-generation/SKILL.md | 8 + 12 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 src/main/java/it/cnr/isti/workflow/manager/configurations/retrievers/SkillsFieldRetriever.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/controllers/SkillsController.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/skills/SkillBinding.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/skills/SkillDefinition.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/skills/SkillPromptService.java create mode 100644 src/main/java/it/cnr/isti/workflow/manager/skills/SkillsCatalogService.java create mode 100644 src/main/resources/skills-catalog.json create mode 100644 src/main/resources/skills/java-development/SKILL.md create mode 100644 src/main/resources/skills/requirements-engineering/SKILL.md create mode 100644 src/main/resources/skills/springboot-development/SKILL.md create mode 100644 src/main/resources/skills/task-generation/SKILL.md diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index d58dfb7..12fbe1e 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -16,4 +16,4 @@ # under the License. wrapperVersion=3.3.2 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/src/main/java/it/cnr/isti/workflow/manager/configurations/retrievers/SkillsFieldRetriever.java b/src/main/java/it/cnr/isti/workflow/manager/configurations/retrievers/SkillsFieldRetriever.java new file mode 100644 index 0000000..2d943f3 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/configurations/retrievers/SkillsFieldRetriever.java @@ -0,0 +1,37 @@ +package it.cnr.isti.workflow.manager.configurations.retrievers; + +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; + +import it.cnr.isti.workflow.manager.skills.SkillsCatalogService; + +@Component +public class SkillsFieldRetriever implements DynamicFieldRetriever { + + private final SkillsCatalogService skillsCatalogService; + + public SkillsFieldRetriever(SkillsCatalogService skillsCatalogService) { + this.skillsCatalogService = skillsCatalogService; + } + + @Override + public String getCategory() { + return "Skills"; + } + + @Override + public List retrieve(String parameter, Map params) { + return switch (parameter) { + case "items" -> skillsCatalogService.listSkills().stream() + .map(skill -> skill.id()) + .sorted() + .toList(); + default -> throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Unknown Skills retriever parameter: " + parameter); + }; + } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/SkillsController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/SkillsController.java new file mode 100644 index 0000000..8b4e1d3 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/SkillsController.java @@ -0,0 +1,41 @@ +package it.cnr.isti.workflow.manager.controllers; + +import java.util.List; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import it.cnr.isti.workflow.manager.skills.SkillDefinition; +import it.cnr.isti.workflow.manager.skills.SkillsCatalogService; + +@RestController +@RequestMapping("/retriever/Skills/definitions") +public class SkillsController { + + private final SkillsCatalogService skillsCatalogService; + + public SkillsController(SkillsCatalogService skillsCatalogService) { + this.skillsCatalogService = skillsCatalogService; + } + + @GetMapping + @Operation(summary = "List skill definitions", description = "Returns the static skills catalog with rendered content.") + public List listDefinitions() { + return skillsCatalogService.listSkills(); + } + + @GetMapping("/{skillId}") + @Operation(summary = "Get skill definition", description = "Returns one skill definition by id.") + public SkillDefinition getDefinition(@PathVariable String skillId) { + try { + return skillsCatalogService.getSkill(skillId); + } catch (IllegalArgumentException ex) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, ex.getMessage(), ex); + } + } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/skills/SkillBinding.java b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillBinding.java new file mode 100644 index 0000000..07d6ca9 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillBinding.java @@ -0,0 +1,21 @@ +package it.cnr.isti.workflow.manager.skills; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import it.cnr.isti.workflow.manager.configurations.annotations.FieldRetriever; +import it.cnr.isti.workflow.manager.configurations.annotations.UiDescription; +import it.cnr.isti.workflow.manager.configurations.annotations.UiLabel; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillBinding( + @NotBlank + @JsonProperty(required = false) + @FieldRetriever(name = "Skills", url = "/retriever/Skills/items") + @UiLabel("skill") + @UiDescription("Local skill loaded from the static skill catalog.") + String skillId) { +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/skills/SkillDefinition.java b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillDefinition.java new file mode 100644 index 0000000..5cab92d --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillDefinition.java @@ -0,0 +1,8 @@ +package it.cnr.isti.workflow.manager.skills; + +public record SkillDefinition( + String id, + String name, + String description, + String content) { +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/skills/SkillPromptService.java b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillPromptService.java new file mode 100644 index 0000000..974aa04 --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillPromptService.java @@ -0,0 +1,40 @@ +package it.cnr.isti.workflow.manager.skills; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class SkillPromptService { + + private final SkillsCatalogService skillsCatalogService; + + public SkillPromptService(SkillsCatalogService skillsCatalogService) { + this.skillsCatalogService = skillsCatalogService; + } + + public String renderSkillsInstructions(List bindings) { + List skills = skillsCatalogService.resolveSkills(bindings); + if (skills.isEmpty()) { + return ""; + } + + List sections = new ArrayList<>(); + sections.add("Apply the following local skills as mandatory operating instructions."); + for (SkillDefinition skill : skills) { + StringBuilder section = new StringBuilder(); + section.append("[SKILL ").append(skill.id()).append("]"); + if (StringUtils.hasText(skill.name())) { + section.append(System.lineSeparator()).append("Name: ").append(skill.name()); + } + if (StringUtils.hasText(skill.description())) { + section.append(System.lineSeparator()).append("Description: ").append(skill.description()); + } + section.append(System.lineSeparator()).append(System.lineSeparator()).append(skill.content().trim()); + sections.add(section.toString()); + } + return String.join(System.lineSeparator() + System.lineSeparator(), sections); + } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/skills/SkillsCatalogService.java b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillsCatalogService.java new file mode 100644 index 0000000..f4926ff --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/skills/SkillsCatalogService.java @@ -0,0 +1,181 @@ +package it.cnr.isti.workflow.manager.skills; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +public class SkillsCatalogService { + + public static final int MAX_SKILLS_PER_BLOCK = 5; + + private final List skills; + private final Map skillById; + + public SkillsCatalogService(ObjectMapper objectMapper, + ResourceLoader resourceLoader, + @Value("${app.skills.catalog.file:}") String catalogLocation) { + this.skills = loadSkills( + Objects.requireNonNull(objectMapper, "objectMapper cannot be null"), + Objects.requireNonNull(resourceLoader, "resourceLoader cannot be null"), + catalogLocation); + LinkedHashMap definitions = new LinkedHashMap<>(); + for (SkillDefinition skill : skills) { + SkillDefinition previous = definitions.putIfAbsent(skill.id(), skill); + if (previous != null) { + throw new IllegalStateException("Duplicate skill id in catalog: " + skill.id()); + } + } + this.skillById = Map.copyOf(definitions); + } + + public List listSkills() { + return skills; + } + + public SkillDefinition getSkill(String id) { + if (!StringUtils.hasText(id)) { + throw new IllegalArgumentException("Skill id cannot be empty"); + } + SkillDefinition skill = skillById.get(id.trim()); + if (skill == null) { + throw new IllegalArgumentException("Unknown skill: " + id); + } + return skill; + } + + public void validateSelectedSkills(List bindings) { + if (bindings == null || bindings.isEmpty()) { + return; + } + if (bindings.size() > MAX_SKILLS_PER_BLOCK) { + throw new IllegalArgumentException( + "At most " + MAX_SKILLS_PER_BLOCK + " skills can be selected for a block"); + } + LinkedHashSet seen = new LinkedHashSet<>(); + for (SkillBinding binding : bindings) { + if (binding == null || !StringUtils.hasText(binding.skillId())) { + throw new IllegalArgumentException("Each selected skill must define a non-empty skillId"); + } + String skillId = binding.skillId().trim(); + if (!seen.add(skillId)) { + throw new IllegalArgumentException("skills must have unique skillId values"); + } + getSkill(skillId); + } + } + + public List resolveSkills(List bindings) { + validateSelectedSkills(bindings); + if (bindings == null || bindings.isEmpty()) { + return List.of(); + } + return bindings.stream() + .map(binding -> getSkill(binding.skillId().trim())) + .toList(); + } + + private List loadSkills(ObjectMapper objectMapper, ResourceLoader resourceLoader, + String catalogLocation) { + try { + Resource resource = resolveCatalogResource(resourceLoader, catalogLocation); + try (InputStream inputStream = resource.getInputStream()) { + SkillsCatalog catalog = objectMapper.readValue(inputStream, SkillsCatalog.class); + if (catalog.skills() == null) { + return List.of(); + } + return catalog.skills().stream() + .map(entry -> toDefinition(entry, resourceLoader)) + .toList(); + } + } catch (Exception ex) { + throw new IllegalStateException( + "Unable to load skills catalog from " + describeLocation(catalogLocation), ex); + } + } + + private SkillDefinition toDefinition(SkillCatalogEntry entry, ResourceLoader resourceLoader) { + if (entry == null || !StringUtils.hasText(entry.id())) { + throw new IllegalStateException("Each skill entry must define a non-empty id"); + } + String content = StringUtils.hasText(entry.content()) + ? entry.content().trim() + : readResourceContent(resolveContentResource(resourceLoader, entry.promptPath())); + if (!StringUtils.hasText(content)) { + throw new IllegalStateException("Skill " + entry.id() + " has empty content"); + } + return new SkillDefinition( + entry.id().trim(), + StringUtils.hasText(entry.name()) ? entry.name().trim() : entry.id().trim(), + StringUtils.hasText(entry.description()) ? entry.description().trim() : null, + content); + } + + private Resource resolveCatalogResource(ResourceLoader resourceLoader, String catalogLocation) { + if (!StringUtils.hasText(catalogLocation)) { + return new ClassPathResource("skills-catalog.json"); + } + String resolvedLocation = catalogLocation.startsWith("classpath:") || catalogLocation.startsWith("file:") + ? catalogLocation + : "file:" + catalogLocation; + Resource resource = resourceLoader.getResource(resolvedLocation); + if (!resource.exists()) { + throw new IllegalStateException("Skills catalog not found at " + resolvedLocation); + } + return resource; + } + + private Resource resolveContentResource(ResourceLoader resourceLoader, String contentLocation) { + if (!StringUtils.hasText(contentLocation)) { + throw new IllegalStateException("Skill catalog entry requires promptPath or inline content"); + } + String resolvedLocation = contentLocation.startsWith("classpath:") || contentLocation.startsWith("file:") + ? contentLocation + : "classpath:" + contentLocation; + Resource resource = resourceLoader.getResource(resolvedLocation); + if (!resource.exists()) { + throw new IllegalStateException("Skill content not found at " + resolvedLocation); + } + return resource; + } + + private String readResourceContent(Resource resource) { + try (InputStream inputStream = resource.getInputStream()) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).trim(); + } catch (IOException ex) { + throw new IllegalStateException("Unable to read skill content from " + resource, ex); + } + } + + private String describeLocation(String catalogLocation) { + return StringUtils.hasText(catalogLocation) ? catalogLocation : "classpath:skills-catalog.json"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record SkillsCatalog(List skills) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record SkillCatalogEntry( + String id, + String name, + String description, + String promptPath, + String content) { + } +} diff --git a/src/main/resources/skills-catalog.json b/src/main/resources/skills-catalog.json new file mode 100644 index 0000000..ec42231 --- /dev/null +++ b/src/main/resources/skills-catalog.json @@ -0,0 +1,28 @@ +{ + "skills": [ + { + "id": "springboot-development", + "name": "Spring Boot Development", + "description": "Guides implementation choices for Spring Boot services, APIs, validation, and tests.", + "promptPath": "classpath:skills/springboot-development/SKILL.md" + }, + { + "id": "java-development", + "name": "Java Development", + "description": "Encourages idiomatic, maintainable Java code with clear structure and safe defaults.", + "promptPath": "classpath:skills/java-development/SKILL.md" + }, + { + "id": "requirements-engineering", + "name": "Requirements Engineering", + "description": "Transform vague feature ideas into clear, testable requirements using EARS format. Capture user stories, define acceptance criteria, identify edge cases, and validate completeness before moving to design.", + "promptPath": "classpath:skills/requirements-engineering/SKILL.md" + }, + { + "id": "task-generation", + "name": "Task Generation", + "description": "Breaks requested work into actionable development tasks with dependency awareness.", + "promptPath": "classpath:skills/task-generation/SKILL.md" + } + ] +} diff --git a/src/main/resources/skills/java-development/SKILL.md b/src/main/resources/skills/java-development/SKILL.md new file mode 100644 index 0000000..34f8d1b --- /dev/null +++ b/src/main/resources/skills/java-development/SKILL.md @@ -0,0 +1,8 @@ +When writing Java code, optimize for readability, safety, and maintainability. + +Apply these rules: +- prefer clear names and small methods over clever compact code +- use immutable collections or defensive copies at module boundaries when appropriate +- fail fast on invalid inputs with explicit exceptions +- keep null handling predictable and avoid hidden side effects +- preserve existing code style and avoid unnecessary abstraction diff --git a/src/main/resources/skills/requirements-engineering/SKILL.md b/src/main/resources/skills/requirements-engineering/SKILL.md new file mode 100644 index 0000000..d112b55 --- /dev/null +++ b/src/main/resources/skills/requirements-engineering/SKILL.md @@ -0,0 +1,266 @@ +--- +name: requirements-engineering +description: Transform vague feature ideas into clear, testable requirements using EARS format. Capture user stories, define acceptance criteria, identify edge cases, and validate completeness before moving to design. +license: MIT +compatibility: Claude Code, Cursor, VS Code, Windsurf +metadata: + category: methodology + complexity: beginner + author: Kiro Team + version: "1.0.0" +--- + +# Requirements Engineering + +Master the art of capturing what needs to be built before diving into how to build it. This skill teaches the EARS (Easy Approach to Requirements Syntax) format for creating clear, testable requirements. + +## When to Use This Skill + +Use requirements engineering when: +- Starting any new feature or project +- Clarifying ambiguous stakeholder requests +- Creating acceptance criteria for user stories +- Documenting system behavior for testing +- Ensuring all team members share understanding + +## The EARS Format + +EARS provides consistent patterns for writing requirements that are specific, testable, and unambiguous. + +### Basic Patterns + +**Event-Response (Most Common):** +``` +WHEN [triggering event] THEN [system] SHALL [required response] +``` + +**Conditional Behavior:** +``` +IF [precondition is met] THEN [system] SHALL [required response] +``` + +**Complex Conditions:** +``` +WHEN [event] AND [additional condition] THEN [system] SHALL [response] +``` + +**Optional Conditions:** +``` +WHEN [event] OR [alternative event] THEN [system] SHALL [response] +``` + +### Advanced Patterns + +**State-Based:** +``` +WHEN [system is in specific state] THEN [system] SHALL [behavior] +``` + +**Performance:** +``` +WHEN [user action] THEN [system] SHALL [respond within X seconds/milliseconds] +``` + +**Security:** +``` +IF [authentication condition] THEN [system] SHALL [security response] +``` + +## Step-by-Step Process + +### Step 1: Capture User Stories + +Format: **As a [role], I want [feature], so that [benefit]** + +Focus on: +- Who is the user? (role) +- What do they want to accomplish? (feature) +- Why does it matter? (benefit/value) + +**Example:** +```markdown +As a returning customer, I want to save my payment methods, so that I can checkout faster in the future. +``` + +### Step 2: Generate Acceptance Criteria + +For each user story, define specific acceptance criteria using EARS: + +**Example for payment methods:** +```markdown +**User Story:** As a returning customer, I want to save my payment methods, so that I can checkout faster. + +**Acceptance Criteria:** +1. WHEN user adds a valid credit card THEN system SHALL securely store card details +2. WHEN user adds a card with invalid number THEN system SHALL display validation error +3. WHEN user has saved cards THEN system SHALL display list during checkout +4. WHEN user selects saved card THEN system SHALL pre-fill payment form +5. WHEN user deletes saved card THEN system SHALL remove card from list +6. IF user is not authenticated THEN system SHALL redirect to login before saving card +7. WHEN user adds card THEN system SHALL mask all but last 4 digits in display +``` + +### Step 3: Identify Edge Cases + +For each requirement, ask: +- What if the input is empty/null? +- What if the input is at boundary values? +- What if the operation fails? +- What if the user is not authorized? +- What if there are concurrent operations? + +**Edge case patterns:** +```markdown +**Error Handling:** +- WHEN [operation fails] THEN system SHALL [display error / retry / log] + +**Boundary Conditions:** +- WHEN [value equals minimum/maximum] THEN system SHALL [specific behavior] + +**Concurrent Access:** +- WHEN [multiple users access same resource] THEN system SHALL [conflict resolution] + +**Empty States:** +- WHEN [collection is empty] THEN system SHALL [display empty state message] +``` + +### Step 4: Validate Requirements + +Use this checklist: + +**Completeness:** +- [ ] All user roles identified and addressed +- [ ] Normal flow scenarios covered +- [ ] Edge cases documented +- [ ] Error cases handled +- [ ] Business rules captured + +**Clarity:** +- [ ] Each requirement uses precise language +- [ ] No ambiguous terms (fast, easy, user-friendly) +- [ ] Technical jargon avoided or defined +- [ ] Expected behaviors are specific + +**Consistency:** +- [ ] EARS format used throughout +- [ ] Terminology consistent across requirements +- [ ] No contradictory requirements +- [ ] Similar scenarios handled similarly + +**Testability:** +- [ ] Each requirement can be verified +- [ ] Success criteria are observable +- [ ] Inputs and expected outputs specified +- [ ] Performance requirements are measurable + +## Common Mistakes to Avoid + +### Mistake 1: Vague Requirements +**Bad:** "System should be fast" +**Good:** "WHEN user submits search THEN system SHALL return results within 2 seconds" + +### Mistake 2: Implementation Details +**Bad:** "System shall use Redis for caching" +**Good:** "WHEN user requests frequently accessed data THEN system SHALL return cached results" + +### Mistake 3: Missing Error Cases +**Bad:** Only documenting happy path +**Good:** Include WHEN/IF statements for all error conditions + +### Mistake 4: Untestable Requirements +**Bad:** "System should be user-friendly" +**Good:** "WHEN new user completes onboarding THEN system SHALL require no more than 3 clicks to reach main dashboard" + +### Mistake 5: Conflicting Requirements +**Bad:** Requirements that contradict each other +**Good:** Review all requirements together, resolve conflicts explicitly + +## Examples + +### Example 1: File Upload Feature + +```markdown +**User Story:** As a user, I want to upload files, so that I can share documents with my team. + +**Acceptance Criteria:** +1. WHEN user selects file under 10MB THEN system SHALL accept file for upload +2. WHEN user selects file over 10MB THEN system SHALL display "file too large (max 10MB)" error +3. WHEN user selects unsupported file type THEN system SHALL display "unsupported format" error with list of allowed types +4. WHEN upload is in progress THEN system SHALL display progress indicator with percentage +5. WHEN upload completes successfully THEN system SHALL display success message with file link +6. WHEN upload fails due to network error THEN system SHALL display retry option +7. IF user is not authenticated THEN system SHALL redirect to login before upload +8. WHEN user uploads file with same name as existing file THEN system SHALL prompt for rename or replace + +**Supported File Types:** PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, GIF +**Maximum File Size:** 10MB +**Maximum Files Per Upload:** 5 +``` + +### Example 2: Search Feature + +```markdown +**User Story:** As a customer, I want to search products, so that I can find items quickly. + +**Acceptance Criteria:** +1. WHEN user enters search term THEN system SHALL display matching products +2. WHEN search returns results THEN system SHALL show result count +3. WHEN search returns no results THEN system SHALL display "no products found" with suggestions +4. WHEN user searches with special characters THEN system SHALL sanitize input and search +5. WHEN user submits empty search THEN system SHALL display validation message +6. WHEN results exceed 20 items THEN system SHALL paginate with 20 items per page +7. WHEN user searches THEN system SHALL return results within 2 seconds +8. WHEN user types in search box THEN system SHALL show autocomplete suggestions after 3 characters + +**Search Fields:** Product name, description, category, SKU +**Minimum Search Length:** 2 characters +``` + +## Requirements Document Template + +```markdown +# Requirements Document: [Feature Name] + +## Overview +[Brief description of the feature and its purpose] + +## User Roles +- [Role 1]: [Description of this user type] +- [Role 2]: [Description of this user type] + +## Requirements + +### Requirement 1: [Name] +**User Story:** As a [role], I want [feature], so that [benefit] + +**Acceptance Criteria:** +1. WHEN [event] THEN system SHALL [response] +2. IF [condition] THEN system SHALL [response] +3. WHEN [event] AND [condition] THEN system SHALL [response] + +**Edge Cases:** +- [Edge case 1 and how it's handled] +- [Edge case 2 and how it's handled] + +### Requirement 2: [Name] +[Continue pattern...] + +## Non-Functional Requirements +- **Performance:** [Specific metrics] +- **Security:** [Security requirements] +- **Accessibility:** [Accessibility standards] + +## Out of Scope +- [Items explicitly not included in this feature] + +## Open Questions +- [Questions that need stakeholder input] +``` + +## Next Steps + +After completing requirements: +1. Review with stakeholders for accuracy +2. Get explicit approval before proceeding +3. Move to Design Phase to create technical architecture +4. Use requirements as foundation for acceptance testing \ No newline at end of file diff --git a/src/main/resources/skills/springboot-development/SKILL.md b/src/main/resources/skills/springboot-development/SKILL.md new file mode 100644 index 0000000..8e14f63 --- /dev/null +++ b/src/main/resources/skills/springboot-development/SKILL.md @@ -0,0 +1,65 @@ +--- +name: java-springboot +description: 'Get best practices for developing applications with Spring Boot.' +--- + +# Spring Boot Best Practices + +Your goal is to help me write high-quality Spring Boot applications by following established best practices. + +## Project Setup & Structure + +- **Build Tool:** Use Maven (`pom.xml`) or Gradle (`build.gradle`) for dependency management. +- **Starters:** Use Spring Boot starters (e.g., `spring-boot-starter-web`, `spring-boot-starter-data-jpa`) to simplify dependency management. +- **Package Structure:** Organize code by feature/domain (e.g., `com.example.app.order`, `com.example.app.user`) rather than by layer (e.g., `com.example.app.controller`, `com.example.app.service`). + +## Dependency Injection & Components + +- **Constructor Injection:** Always use constructor-based injection for required dependencies. This makes components easier to test and dependencies explicit. +- **Immutability:** Declare dependency fields as `private final`. +- **Component Stereotypes:** Use `@Component`, `@Service`, `@Repository`, and `@Controller`/`@RestController` annotations appropriately to define beans. + +## Configuration + +- **Externalized Configuration:** Use `application.yml` (or `application.properties`) for configuration. YAML is often preferred for its readability and hierarchical structure. +- **Type-Safe Properties:** Use `@ConfigurationProperties` to bind configuration to strongly-typed Java objects. +- **Profiles:** Use Spring Profiles (`application-dev.yml`, `application-prod.yml`) to manage environment-specific configurations. +- **Secrets Management:** Do not hardcode secrets. Use environment variables, or a dedicated secret management tool like HashiCorp Vault or AWS Secrets Manager. + +## Web Layer (Controllers) + +- **RESTful APIs:** Design clear and consistent RESTful endpoints. +- **DTOs (Data Transfer Objects):** Use DTOs to expose and consume data in the API layer. Do not expose JPA entities directly to the client. +- **Validation:** Use Java Bean Validation (JSR 380) with annotations (`@Valid`, `@NotNull`, `@Size`) on DTOs to validate request payloads. +- **Error Handling:** Implement a global exception handler using `@ControllerAdvice` and `@ExceptionHandler` to provide consistent error responses. + +## Service Layer + +- **Business Logic:** Encapsulate all business logic within `@Service` classes. +- **Statelessness:** Services should be stateless. +- **Transaction Management:** Use `@Transactional` on service methods to manage database transactions declaratively. Apply it at the most granular level necessary. + +## Data Layer (Repositories) + +- **Spring Data JPA:** Use Spring Data JPA repositories by extending `JpaRepository` or `CrudRepository` for standard database operations. +- **Custom Queries:** For complex queries, use `@Query` or the JPA Criteria API. +- **Projections:** Use DTO projections to fetch only the necessary data from the database. + +## Logging + +- **SLF4J:** Use the SLF4J API for logging. +- **Logger Declaration:** `private static final Logger logger = LoggerFactory.getLogger(MyClass.class);` +- **Parameterized Logging:** Use parameterized messages (`logger.info("Processing user {}...", userId);`) instead of string concatenation to improve performance. + +## Testing + +- **Unit Tests:** Write unit tests for services and components using JUnit 5 and a mocking framework like Mockito. +- **Integration Tests:** Use `@SpringBootTest` for integration tests that load the Spring application context. +- **Test Slices:** Use test slice annotations like `@WebMvcTest` (for controllers) or `@DataJpaTest` (for repositories) to test specific parts of the application in isolation. +- **Testcontainers:** Consider using Testcontainers for reliable integration tests with real databases, message brokers, etc. + +## Security + +- **Spring Security:** Use Spring Security for authentication and authorization. +- **Password Encoding:** Always encode passwords using a strong hashing algorithm like BCrypt. +- **Input Sanitization:** Prevent SQL injection by using Spring Data JPA or parameterized queries. Prevent Cross-Site Scripting (XSS) by properly encoding output. \ No newline at end of file diff --git a/src/main/resources/skills/task-generation/SKILL.md b/src/main/resources/skills/task-generation/SKILL.md new file mode 100644 index 0000000..0976cb8 --- /dev/null +++ b/src/main/resources/skills/task-generation/SKILL.md @@ -0,0 +1,8 @@ +When the request implies multiple implementation steps, organize the work into practical development tasks. + +Apply these rules: +- break work into small executable tasks +- order tasks by dependency and risk +- separate implementation, validation, and cleanup +- keep tasks concrete enough that a developer could execute them directly +- avoid speculative tasks that do not materially advance the requested outcome From d4b578099c2a355a3ae5fd7bd5bb84a13ab0cdad Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 17 Apr 2026 14:03:51 +0200 Subject: [PATCH 2/4] Step 3: Upgrade Java to 25 and Update classgraph - Compile: SUCCESS - java.version property updated from 21 to 25 in pom.xml - classgraph upgraded from 4.8.159 to 4.8.184 (Java 25 module compatibility) - Compilation passes cleanly with Java 25.0.1 (Temurin) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3917f8f..3492330 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ - 21 + 25 @@ -78,7 +78,7 @@ io.github.classgraph classgraph - 4.8.159 + 4.8.184 From b6a4adb89f82ac0c47f832d2262b8cb2bda6878b Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 17 Apr 2026 14:11:02 +0200 Subject: [PATCH 3/4] Update Dockerfile for Java 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Build stage: maven:3.9.8-eclipse-temurin-21 → maven:3.9-eclipse-temurin-25 - Runtime stage: eclipse-temurin:21 → eclipse-temurin:25-jre (JRE for smaller image) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3474028..dbe98df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM maven:3.9.8-eclipse-temurin-21 AS build +FROM maven:3.9-eclipse-temurin-25 AS build WORKDIR /app COPY . . RUN ./mvnw clean package -DskipTests -FROM eclipse-temurin:21 +FROM eclipse-temurin:25-jre COPY --from=build /app/target/humainflow.jar /app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] From c83ede89e5c032753e9918a75887c09714c779f7 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 17 Apr 2026 17:14:35 +0200 Subject: [PATCH 4/4] Make flow seed import idempotent on restart --- .../manager/flows/FlowImportComponent.java | 8 +-- .../flows/FlowImportComponentTest.java | 65 ++++++++++++++++++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/FlowImportComponent.java b/src/main/java/it/cnr/isti/workflow/manager/flows/FlowImportComponent.java index a5f3cd9..505f897 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/FlowImportComponent.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/FlowImportComponent.java @@ -94,9 +94,6 @@ public class FlowImportComponent { ? importedFlow.lastUpdateAt() : LocalDateTime.now(); - if (existing.isPresent() && importedFlow.id() != null && !importedFlow.id().isBlank()) { - entity.setId(importedFlow.id()); - } entity.setName(importedFlow.name()); entity.setDescription(importedFlow.description()); entity.setOwner(importedFlow.owner()); @@ -112,7 +109,10 @@ public class FlowImportComponent { private Optional findExisting(ImportedFlow importedFlow) { if (importedFlow.id() != null && !importedFlow.id().isBlank()) { - return flowRepository.findById(importedFlow.id()); + Optional existingById = flowRepository.findById(importedFlow.id()); + if (existingById.isPresent()) { + return existingById; + } } return flowRepository.findByOwnerAndName(importedFlow.owner(), importedFlow.name()); } diff --git a/src/test/java/it/cnr/isti/workflow/manager/flows/FlowImportComponentTest.java b/src/test/java/it/cnr/isti/workflow/manager/flows/FlowImportComponentTest.java index 6291499..1217cbe 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/flows/FlowImportComponentTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/flows/FlowImportComponentTest.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -53,7 +54,7 @@ public class FlowImportComponentTest { .build())) .build(); ImportedFlow importedFlow = new ImportedFlow( - "seed-flow-id", + null, "Imported Flow", "Flow loaded from JSON", "testuser", @@ -69,6 +70,7 @@ public class FlowImportComponentTest { ReflectionTestUtils.setField(flowImportComponent, "enabled", true); ReflectionTestUtils.setField(flowImportComponent, "path", tempDir.toString()); + flowImportComponent.start(); flowImportComponent.start(); FlowEntity saved = flowRepository.findByOwnerAndName("testuser", "Imported Flow").orElse(null); @@ -79,5 +81,66 @@ public class FlowImportComponentTest { assertTrue(saved.isPublished()); assertNotNull(saved.getFlow()); assertEquals(1, saved.getFlow().getBlocks().size()); + assertEquals(1, flowRepository.count()); + } + + @Test + public void importFlowWithIdUpdatesLegacyEntryWithoutDuplicatingIt() throws Exception { + FlowData originalFlow = FlowData.builder() + .block(llmBlockFactory.create(LLMBlockConfiguration.builder() + .name("legacy-llm") + .prompt("Original prompt") + .llmDescriptor(LLMDescriptor.builder() + .provider("testProvider") + .model("testModel") + .build()) + .build())) + .build(); + FlowEntity legacyEntity = FlowEntity.builder() + .id(UUID.randomUUID().toString()) + .name("Imported Flow") + .description("Legacy description") + .owner("testuser") + .createdAt(LocalDateTime.of(2026, 3, 1, 10, 0)) + .lastUpdateAt(LocalDateTime.of(2026, 3, 1, 10, 0)) + .flow(originalFlow) + .build(); + flowRepository.save(legacyEntity); + + FlowData updatedFlow = FlowData.builder() + .block(llmBlockFactory.create(LLMBlockConfiguration.builder() + .name("updated-llm") + .prompt("Updated prompt") + .llmDescriptor(LLMDescriptor.builder() + .provider("testProvider") + .model("testModel") + .build()) + .build())) + .build(); + ImportedFlow importedFlow = new ImportedFlow( + "seed-flow-id", + "Imported Flow", + "Updated description", + "testuser", + LocalDateTime.of(2026, 3, 1, 10, 0), + LocalDateTime.of(2026, 3, 2, 11, 0), + true, + false, + updatedFlow); + + Files.writeString(tempDir.resolve("flows.json"), + ObjectMapperHolder.mapper.writeValueAsString(List.of(importedFlow))); + + ReflectionTestUtils.setField(flowImportComponent, "enabled", true); + ReflectionTestUtils.setField(flowImportComponent, "path", tempDir.toString()); + + flowImportComponent.start(); + + FlowEntity saved = flowRepository.findByOwnerAndName("testuser", "Imported Flow").orElse(null); + assertNotNull(saved); + assertEquals(legacyEntity.getId(), saved.getId()); + assertEquals("Updated description", saved.getDescription()); + assertTrue(saved.isPublished()); + assertEquals(1, flowRepository.count()); } }