From adcb74f7a04a3aafcdfd86ea4543d2f793143c9f Mon Sep 17 00:00:00 2001 From: Kosta Petan Date: Mon, 30 Mar 2026 09:31:06 +0200 Subject: [PATCH 1/4] chore: remove sample PRD/FRD specs and their implementation Shells should be empty specs, implementation, tests, andscaffolds features are generated by the spec2cloud orchestrator from user PRDs. Removed: - PRD and FRD spec documents - Gherkin feature files (specs/features/) - Backend implementation (routes, models, services, middleware) - Frontend pages and components tied to the sample app - E2E test specs (Playwright) - BDD step definitions - Generated docs (dotnet: specs/docs/) Kept: - Shell scaffolding (project configs, Dockerfiles, package.json, pom.xml, pyproject.toml) - Infrastructure (Bicep, azure.yaml, apphost.cs) - spec2cloud framework (.spec2cloud/, skills, AGENTS.md, copilot-instructions) - Test framework configs (playwright.config, vitest.config, cucumber.js) - DevContainer setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- e2e/admin.spec.ts | 41 --- e2e/auth.spec.ts | 46 ---- e2e/fixtures.ts | 89 ------- e2e/landing.spec.ts | 22 -- e2e/profile.spec.ts | 23 -- .../java/com/spec2cloud/api/Application.java | 12 - .../com/spec2cloud/api/config/JwtConfig.java | 7 - .../spec2cloud/api/config/SecurityConfig.java | 46 ---- .../api/controller/AdminController.java | 40 --- .../api/controller/AuthController.java | 84 ------- .../api/controller/HealthController.java | 16 -- .../api/middleware/JwtAuthFilter.java | 57 ----- .../spec2cloud/api/model/ErrorResponse.java | 4 - .../spec2cloud/api/model/LoginRequest.java | 4 - .../spec2cloud/api/model/MessageResponse.java | 4 - .../spec2cloud/api/model/RegisterRequest.java | 4 - .../java/com/spec2cloud/api/model/User.java | 12 - .../spec2cloud/api/model/UserResponse.java | 10 - .../spec2cloud/api/service/AuthService.java | 121 --------- .../com/spec2cloud/api/service/UserStore.java | 57 ----- .../src/main/resources/application.properties | 5 - .../spec2cloud/api/AdminControllerTest.java | 67 ----- .../spec2cloud/api/AuthControllerTest.java | 234 ------------------ src/web/src/app/admin/page.tsx | 95 ------- src/web/src/app/components/LoginForm.tsx | 94 ------- src/web/src/app/components/NavBar.tsx | 54 ---- src/web/src/app/components/RegisterForm.tsx | 105 -------- src/web/src/app/globals.css | 26 -- src/web/src/app/hooks/useAuth.ts | 28 --- src/web/src/app/layout.tsx | 36 --- src/web/src/app/login/page.tsx | 12 - src/web/src/app/page.tsx | 41 --- src/web/src/app/profile/page.tsx | 88 ------- src/web/src/app/register/page.tsx | 9 - tests/features/auth.feature | 68 ----- tests/features/profile.feature | 19 -- tests/features/rbac.feature | 33 --- 37 files changed, 1713 deletions(-) delete mode 100644 e2e/admin.spec.ts delete mode 100644 e2e/auth.spec.ts delete mode 100644 e2e/fixtures.ts delete mode 100644 e2e/landing.spec.ts delete mode 100644 e2e/profile.spec.ts delete mode 100644 src/api/src/main/java/com/spec2cloud/api/Application.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/config/JwtConfig.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/config/SecurityConfig.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/controller/AdminController.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/controller/AuthController.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/middleware/JwtAuthFilter.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/model/ErrorResponse.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/model/LoginRequest.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/model/MessageResponse.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/model/RegisterRequest.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/model/User.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/model/UserResponse.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/service/AuthService.java delete mode 100644 src/api/src/main/java/com/spec2cloud/api/service/UserStore.java delete mode 100644 src/api/src/main/resources/application.properties delete mode 100644 src/api/src/test/java/com/spec2cloud/api/AdminControllerTest.java delete mode 100644 src/api/src/test/java/com/spec2cloud/api/AuthControllerTest.java delete mode 100644 src/web/src/app/admin/page.tsx delete mode 100644 src/web/src/app/components/LoginForm.tsx delete mode 100644 src/web/src/app/components/NavBar.tsx delete mode 100644 src/web/src/app/components/RegisterForm.tsx delete mode 100644 src/web/src/app/globals.css delete mode 100644 src/web/src/app/hooks/useAuth.ts delete mode 100644 src/web/src/app/layout.tsx delete mode 100644 src/web/src/app/login/page.tsx delete mode 100644 src/web/src/app/page.tsx delete mode 100644 src/web/src/app/profile/page.tsx delete mode 100644 src/web/src/app/register/page.tsx delete mode 100644 tests/features/auth.feature delete mode 100644 tests/features/profile.feature delete mode 100644 tests/features/rbac.feature diff --git a/e2e/admin.spec.ts b/e2e/admin.spec.ts deleted file mode 100644 index 24e9749..0000000 --- a/e2e/admin.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { test, expect } from "./fixtures"; - -test.describe("Admin Page", () => { - test("first user (admin) can see user table", async ({ page, loginUser }) => { - // First registered user automatically becomes admin - const admin = await loginUser(); - - await page.goto("/admin"); - await expect(page.getByRole("heading", { name: "Users" })).toBeVisible(); - await expect(page.getByRole("table")).toBeVisible(); - await expect(page.getByRole("cell", { name: admin.username })).toBeVisible(); - }); - - test("non-admin user sees access denied", async ({ page, loginUser }) => { - // Register first user (becomes admin) - const admin = await loginUser(); - - // Logout admin - await page.getByRole("button", { name: "Logout" }).click(); - await expect(page).toHaveURL(/\/login/); - - // Register and login second user (non-admin) - await page.goto("/register"); - const username = `regular${Date.now()}`; - const password = "Test1234!secure"; - await page.getByLabel("Username").fill(username); - await page.getByLabel("Password", { exact: true }).fill(password); - await page.getByLabel("Confirm Password").fill(password); - await page.getByRole("button", { name: "Register" }).click(); - await expect(page).toHaveURL(/\/login/); - - await page.getByLabel("Username").fill(username); - await page.getByLabel("Password").fill(password); - await page.getByRole("button", { name: "Log in" }).click(); - await expect(page).toHaveURL(/\/profile/); - - // Try to access admin page - await page.goto("/admin"); - await expect(page.getByText("Access Denied")).toBeVisible(); - }); -}); diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts deleted file mode 100644 index 3076dea..0000000 --- a/e2e/auth.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { test, expect } from "./fixtures"; - -test.describe("Authentication", () => { - test("register a new user and redirect to login", async ({ page, registerUser }) => { - const creds = await registerUser(); - - // Should land on login page with success message - await expect(page).toHaveURL(/\/login\?registered=true/); - await expect(page.getByText("Registration successful")).toBeVisible(); - }); - - test("login with valid credentials redirects to profile", async ({ page, loginUser }) => { - const creds = await loginUser(); - - await expect(page).toHaveURL(/\/profile/); - await expect(page.getByText(creds.username)).toBeVisible(); - }); - - test("login with invalid credentials shows error", async ({ page, registerUser }) => { - const creds = await registerUser(); - - await page.goto("/login"); - await page.getByLabel("Username").fill(creds.username); - await page.getByLabel("Password").fill("WrongPassword123!"); - await page.getByRole("button", { name: "Log in" }).click(); - - await expect(page.getByText(/invalid/i)).toBeVisible(); - await expect(page).toHaveURL(/\/login/); - }); - - test("logout redirects to login page", async ({ page, loginUser }) => { - await loginUser(); - - // Click logout button in NavBar - await page.getByRole("button", { name: "Logout" }).click(); - - // Should redirect away from profile - await expect(page).toHaveURL(/\/login/); - }); - - test("unauthenticated access to /profile redirects to login", async ({ page }) => { - await page.goto("/profile"); - - await expect(page).toHaveURL(/\/login/); - }); -}); diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts deleted file mode 100644 index b5f5896..0000000 --- a/e2e/fixtures.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { test as base, expect, type Page } from "@playwright/test"; - -/** Credentials for a test user. */ -interface TestUser { - username: string; - password: string; -} - -/** Extended test fixtures with auth helpers and auto-cleanup. */ -interface Fixtures { - /** Register a new user via the UI. Returns the credentials used. */ - registerUser: (opts?: Partial) => Promise; - /** Register then log in a user via the UI. Returns the credentials. */ - loginUser: (opts?: Partial) => Promise; - /** Backend API base URL for direct HTTP calls. */ - apiURL: string; -} - -let counter = 0; - -function uniqueUser(overrides?: Partial): TestUser { - counter += 1; - return { - username: overrides?.username ?? `testuser${Date.now()}${counter}`, - password: overrides?.password ?? "Test1234!secure", - }; -} - -export const test = base.extend({ - apiURL: [process.env.API_URL ?? "http://localhost:5000", { option: true }], - - registerUser: async ({ page }, use) => { - const registered: TestUser[] = []; - - const fn = async (opts?: Partial) => { - const creds = uniqueUser(opts); - - await page.goto("/register"); - await page.getByLabel("Username").fill(creds.username); - await page.getByLabel("Password", { exact: true }).fill(creds.password); - await page.getByLabel("Confirm Password").fill(creds.password); - await page.getByRole("button", { name: "Register" }).click(); - - // Wait for redirect to login page with success banner - await expect(page).toHaveURL(/\/login/); - registered.push(creds); - return creds; - }; - - await use(fn); - - // Cleanup: remove test users via the API (best-effort) - for (const u of registered) { - try { - await page.request.post(`${process.env.API_URL ?? "http://localhost:5000"}/api/auth/login`, { - data: { username: u.username, password: u.password }, - }); - } catch { - // ignore cleanup errors - } - } - }, - - loginUser: async ({ page }, use) => { - const fn = async (opts?: Partial) => { - const creds = uniqueUser(opts); - - // Register - await page.goto("/register"); - await page.getByLabel("Username").fill(creds.username); - await page.getByLabel("Password", { exact: true }).fill(creds.password); - await page.getByLabel("Confirm Password").fill(creds.password); - await page.getByRole("button", { name: "Register" }).click(); - await expect(page).toHaveURL(/\/login/); - - // Login - await page.getByLabel("Username").fill(creds.username); - await page.getByLabel("Password").fill(creds.password); - await page.getByRole("button", { name: "Log in" }).click(); - await expect(page).toHaveURL(/\/profile/); - - return creds; - }; - - await use(fn); - }, -}); - -export { expect }; diff --git a/e2e/landing.spec.ts b/e2e/landing.spec.ts deleted file mode 100644 index f45cc34..0000000 --- a/e2e/landing.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { test, expect } from "./fixtures"; - -test.describe("Landing Page", () => { - test("shows login and register links when not logged in", async ({ page }) => { - await page.goto("/"); - - await expect(page.getByRole("heading", { name: "UserAuth" })).toBeVisible(); - await expect(page.getByRole("link", { name: "Login" })).toBeVisible(); - await expect(page.getByRole("link", { name: "Register" })).toBeVisible(); - }); - - test("shows profile link when logged in", async ({ page, loginUser }) => { - await loginUser(); - - await page.goto("/"); - await expect(page.getByRole("link", { name: "Go to Profile" })).toBeVisible(); - - // Login/Register links should not be visible - await expect(page.getByRole("link", { name: "Login" })).not.toBeVisible(); - await expect(page.getByRole("link", { name: "Register" })).not.toBeVisible(); - }); -}); diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts deleted file mode 100644 index c49c7bf..0000000 --- a/e2e/profile.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { test, expect } from "./fixtures"; - -test.describe("Profile Page", () => { - test("shows username and role badge", async ({ page, loginUser }) => { - const creds = await loginUser(); - - await expect(page).toHaveURL(/\/profile/); - await expect(page.getByText(creds.username)).toBeVisible(); - await expect(page.getByTestId("role-badge")).toBeVisible(); - }); - - test("displays logout button in navigation", async ({ page, loginUser }) => { - await loginUser(); - - await expect(page.getByRole("button", { name: "Logout" })).toBeVisible(); - }); - - test("shows member since date", async ({ page, loginUser }) => { - await loginUser(); - - await expect(page.getByText(/Member since/)).toBeVisible(); - }); -}); diff --git a/src/api/src/main/java/com/spec2cloud/api/Application.java b/src/api/src/main/java/com/spec2cloud/api/Application.java deleted file mode 100644 index 03bf2bf..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/Application.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.spec2cloud.api; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/config/JwtConfig.java b/src/api/src/main/java/com/spec2cloud/api/config/JwtConfig.java deleted file mode 100644 index 8aff161..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/config/JwtConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.spec2cloud.api.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "jwt") -public record JwtConfig(String secret, long expiration) { -} diff --git a/src/api/src/main/java/com/spec2cloud/api/config/SecurityConfig.java b/src/api/src/main/java/com/spec2cloud/api/config/SecurityConfig.java deleted file mode 100644 index f55942a..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/config/SecurityConfig.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.spec2cloud.api.config; - -import com.spec2cloud.api.middleware.JwtAuthFilter; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; - -import java.util.List; - -@Configuration -@EnableWebSecurity -@EnableConfigurationProperties(JwtConfig.class) -public class SecurityConfig { - - private final JwtAuthFilter jwtAuthFilter; - - public SecurityConfig(JwtAuthFilter jwtAuthFilter) { - this.jwtAuthFilter = jwtAuthFilter; - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .cors(cors -> cors.configurationSource(request -> { - var config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of("*")); - config.setAllowedMethods(List.of("*")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(true); - return config; - })) - .csrf(AbstractHttpConfigurer::disable) - .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/controller/AdminController.java b/src/api/src/main/java/com/spec2cloud/api/controller/AdminController.java deleted file mode 100644 index 033b605..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/controller/AdminController.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.spec2cloud.api.controller; - -import com.spec2cloud.api.model.ErrorResponse; -import com.spec2cloud.api.model.UserResponse; -import com.spec2cloud.api.service.UserStore; -import io.jsonwebtoken.Claims; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/admin") -public class AdminController { - - private final UserStore userStore; - - public AdminController(UserStore userStore) { - this.userStore = userStore; - } - - @GetMapping("/users") - public ResponseEntity listUsers(Authentication authentication) { - if (authentication == null || !(authentication.getPrincipal() instanceof Claims claims)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Not authenticated")); - } - - String role = claims.get("role", String.class); - if (!"admin".equals(role)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse("Forbidden")); - } - - var users = userStore.findAll().stream() - .map(UserResponse::from) - .toList(); - return ResponseEntity.ok(users); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/controller/AuthController.java b/src/api/src/main/java/com/spec2cloud/api/controller/AuthController.java deleted file mode 100644 index 779bcf4..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/controller/AuthController.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.spec2cloud.api.controller; - -import com.spec2cloud.api.model.*; -import com.spec2cloud.api.service.AuthService; -import com.spec2cloud.api.service.AuthService.LoginResult; -import com.spec2cloud.api.service.AuthService.RegisterResult; -import io.jsonwebtoken.Claims; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/auth") -public class AuthController { - - private static final int COOKIE_MAX_AGE = 86400; // 24 hours in seconds - - private final AuthService authService; - - public AuthController(AuthService authService) { - this.authService = authService; - } - - @PostMapping("/register") - public ResponseEntity register(@RequestBody RegisterRequest req, HttpServletResponse response) { - var result = authService.register(req.username(), req.password()); - - if (result instanceof RegisterResult.Success s) { - addTokenCookie(response, authService.generateToken(s.user())); - return ResponseEntity.status(HttpStatus.CREATED).body( - new MessageResponse("Registration successful")); - } else if (result instanceof RegisterResult.ValidationError e) { - return ResponseEntity.badRequest().body(new ErrorResponse(e.message())); - } else { - return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse("Username already exists")); - } - } - - @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest req, HttpServletResponse response) { - var result = authService.login(req.username(), req.password()); - - if (result instanceof LoginResult.Success s) { - addTokenCookie(response, authService.generateToken(s.user())); - return ResponseEntity.ok(new MessageResponse("Login successful")); - } else if (result instanceof LoginResult.BadCredentials e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(e.message())); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Invalid credentials")); - } - - @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - var cookie = new Cookie("token", ""); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); - return ResponseEntity.ok(new MessageResponse("Logged out successfully")); - } - - @GetMapping("/me") - public ResponseEntity me(Authentication authentication) { - if (authentication == null || !(authentication.getPrincipal() instanceof Claims claims)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Not authenticated")); - } - return authService.getCurrentUser((String) authentication.getCredentials()) - .>map(user -> ResponseEntity.ok(UserResponse.from(user))) - .orElse(ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Not authenticated"))); - } - - private void addTokenCookie(HttpServletResponse response, String token) { - var cookie = new Cookie("token", token); - cookie.setHttpOnly(true); - cookie.setSecure(true); - cookie.setPath("/"); - cookie.setMaxAge(COOKIE_MAX_AGE); - response.addCookie(cookie); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java b/src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java deleted file mode 100644 index ba3cc19..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.spec2cloud.api.controller; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@RestController -public class HealthController { - - @GetMapping("/health") - public ResponseEntity> health() { - return ResponseEntity.ok(Map.of("status", "healthy")); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/middleware/JwtAuthFilter.java b/src/api/src/main/java/com/spec2cloud/api/middleware/JwtAuthFilter.java deleted file mode 100644 index 79227cb..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/middleware/JwtAuthFilter.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.spec2cloud.api.middleware; - -import com.spec2cloud.api.service.AuthService; -import io.jsonwebtoken.Claims; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -@Component -public class JwtAuthFilter extends OncePerRequestFilter { - - private final AuthService authService; - - public JwtAuthFilter(AuthService authService) { - this.authService = authService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - extractTokenFromCookies(request).ifPresent(token -> - authService.validateToken(token).ifPresent(claims -> { - var authorities = List.of( - new SimpleGrantedAuthority("ROLE_" + claims.get("role", String.class).toUpperCase()) - ); - var authentication = new UsernamePasswordAuthenticationToken( - claims, token, authorities - ); - SecurityContextHolder.getContext().setAuthentication(authentication); - }) - ); - - filterChain.doFilter(request, response); - } - - private java.util.Optional extractTokenFromCookies(HttpServletRequest request) { - if (request.getCookies() == null) return java.util.Optional.empty(); - return Arrays.stream(request.getCookies()) - .filter(c -> "token".equals(c.getName())) - .map(Cookie::getValue) - .filter(v -> !v.isBlank()) - .findFirst(); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/model/ErrorResponse.java b/src/api/src/main/java/com/spec2cloud/api/model/ErrorResponse.java deleted file mode 100644 index 83904a4..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/model/ErrorResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.spec2cloud.api.model; - -public record ErrorResponse(String error) { -} diff --git a/src/api/src/main/java/com/spec2cloud/api/model/LoginRequest.java b/src/api/src/main/java/com/spec2cloud/api/model/LoginRequest.java deleted file mode 100644 index 2ffe43d..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/model/LoginRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.spec2cloud.api.model; - -public record LoginRequest(String username, String password) { -} diff --git a/src/api/src/main/java/com/spec2cloud/api/model/MessageResponse.java b/src/api/src/main/java/com/spec2cloud/api/model/MessageResponse.java deleted file mode 100644 index 222c978..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/model/MessageResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.spec2cloud.api.model; - -public record MessageResponse(String message) { -} diff --git a/src/api/src/main/java/com/spec2cloud/api/model/RegisterRequest.java b/src/api/src/main/java/com/spec2cloud/api/model/RegisterRequest.java deleted file mode 100644 index 0414c48..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/model/RegisterRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.spec2cloud.api.model; - -public record RegisterRequest(String username, String password) { -} diff --git a/src/api/src/main/java/com/spec2cloud/api/model/User.java b/src/api/src/main/java/com/spec2cloud/api/model/User.java deleted file mode 100644 index 45b0f9e..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/model/User.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.spec2cloud.api.model; - -import java.time.Instant; - -public record User( - String id, - String username, - String passwordHash, - String role, - Instant createdAt -) { -} diff --git a/src/api/src/main/java/com/spec2cloud/api/model/UserResponse.java b/src/api/src/main/java/com/spec2cloud/api/model/UserResponse.java deleted file mode 100644 index d617358..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/model/UserResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.spec2cloud.api.model; - -import java.time.Instant; - -public record UserResponse(String username, String role, Instant createdAt) { - - public static UserResponse from(User user) { - return new UserResponse(user.username(), user.role(), user.createdAt()); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/service/AuthService.java b/src/api/src/main/java/com/spec2cloud/api/service/AuthService.java deleted file mode 100644 index 030db7b..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/service/AuthService.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.spec2cloud.api.service; - -import com.spec2cloud.api.config.JwtConfig; -import com.spec2cloud.api.model.User; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Service; - -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.Date; -import java.util.Optional; -import java.util.UUID; -import java.util.regex.Pattern; - -@Service -public class AuthService { - - private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,30}$"); - private static final int MIN_PASSWORD_LENGTH = 8; - - private final UserStore userStore; - private final JwtConfig jwtConfig; - private final SecretKey signingKey; - private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10); - - public AuthService(UserStore userStore, JwtConfig jwtConfig) { - this.userStore = userStore; - this.jwtConfig = jwtConfig; - this.signingKey = Keys.hmacShaKeyFor(jwtConfig.secret().getBytes(StandardCharsets.UTF_8)); - } - - // ---- Registration ---- - - public sealed interface RegisterResult { - record Success(User user) implements RegisterResult {} - record ValidationError(String message) implements RegisterResult {} - record Duplicate() implements RegisterResult {} - } - - public RegisterResult register(String username, String password) { - if (username == null || username.isBlank() || password == null || password.isBlank()) { - return new RegisterResult.ValidationError("Username and password are required"); - } - if (!USERNAME_PATTERN.matcher(username).matches()) { - return new RegisterResult.ValidationError( - "Username must be between 3 and 30 characters and contain only letters, numbers, and underscores"); - } - if (password.length() < MIN_PASSWORD_LENGTH) { - return new RegisterResult.ValidationError("Password must be at least 8 characters"); - } - if (userStore.findByUsername(username).isPresent()) { - return new RegisterResult.Duplicate(); - } - - String role = userStore.count() == 0 ? "admin" : "user"; - var user = new User( - UUID.randomUUID().toString(), - username, - passwordEncoder.encode(password), - role, - Instant.now() - ); - userStore.save(user); - return new RegisterResult.Success(user); - } - - // ---- Login ---- - - public sealed interface LoginResult { - record Success(User user) implements LoginResult {} - record BadCredentials(String message) implements LoginResult {} - } - - public LoginResult login(String username, String password) { - if (username == null || username.isBlank() || password == null || password.isBlank()) { - return new LoginResult.BadCredentials("Username and password are required"); - } - Optional maybeUser = userStore.findByUsername(username); - if (maybeUser.isEmpty() || !passwordEncoder.matches(password, maybeUser.get().passwordHash())) { - return new LoginResult.BadCredentials("Invalid username or password"); - } - return new LoginResult.Success(maybeUser.get()); - } - - // ---- JWT ---- - - public String generateToken(User user) { - Instant now = Instant.now(); - return Jwts.builder() - .subject(user.id()) - .claim("username", user.username()) - .claim("role", user.role()) - .issuedAt(Date.from(now)) - .expiration(Date.from(now.plusMillis(jwtConfig.expiration()))) - .signWith(signingKey) - .compact(); - } - - public Optional validateToken(String token) { - try { - Claims claims = Jwts.parser() - .verifyWith(signingKey) - .build() - .parseSignedClaims(token) - .getPayload(); - return Optional.of(claims); - } catch (JwtException | IllegalArgumentException e) { - return Optional.empty(); - } - } - - public Optional getCurrentUser(String token) { - return validateToken(token) - .flatMap(claims -> userStore.findById(claims.getSubject())); - } -} diff --git a/src/api/src/main/java/com/spec2cloud/api/service/UserStore.java b/src/api/src/main/java/com/spec2cloud/api/service/UserStore.java deleted file mode 100644 index b7e2ab3..0000000 --- a/src/api/src/main/java/com/spec2cloud/api/service/UserStore.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.spec2cloud.api.service; - -import com.spec2cloud.api.model.User; -import org.springframework.stereotype.Service; - -import java.util.Collection; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -/** - * In-memory user store backed by ConcurrentHashMap. - *

- * To migrate to Cosmos DB: - * 1. Replace ConcurrentHashMap with CosmosContainer operations - * 2. Change save() to use container.upsertItem() - * 3. Change findByUsername() to a SQL query: SELECT * FROM c WHERE c.username = @username - * 4. Change findById() to container.readItem() - * 5. Change findAll() to a SQL query: SELECT * FROM c - *

- */ -@Service -public class UserStore { - - // Keyed by user ID - private final ConcurrentHashMap usersById = new ConcurrentHashMap<>(); - // Secondary index: username → user ID (for fast lookups) - private final ConcurrentHashMap usernameIndex = new ConcurrentHashMap<>(); - - public Optional findByUsername(String username) { - String id = usernameIndex.get(username); - return id != null ? Optional.ofNullable(usersById.get(id)) : Optional.empty(); - } - - public Optional findById(String id) { - return Optional.ofNullable(usersById.get(id)); - } - - public Collection findAll() { - return usersById.values(); - } - - public User save(User user) { - usersById.put(user.id(), user); - usernameIndex.put(user.username(), user.id()); - return user; - } - - public int count() { - return usersById.size(); - } - - /** Visible for testing — clears all data. */ - public void clear() { - usersById.clear(); - usernameIndex.clear(); - } -} diff --git a/src/api/src/main/resources/application.properties b/src/api/src/main/resources/application.properties deleted file mode 100644 index 4bbb334..0000000 --- a/src/api/src/main/resources/application.properties +++ /dev/null @@ -1,5 +0,0 @@ -server.port=8080 - -# JWT configuration -jwt.secret=spec2cloud-dev-jwt-secret-change-in-production -jwt.expiration=86400000 diff --git a/src/api/src/test/java/com/spec2cloud/api/AdminControllerTest.java b/src/api/src/test/java/com/spec2cloud/api/AdminControllerTest.java deleted file mode 100644 index e857b63..0000000 --- a/src/api/src/test/java/com/spec2cloud/api/AdminControllerTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.spec2cloud.api; - -import com.spec2cloud.api.service.UserStore; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import jakarta.servlet.http.Cookie; - -import static org.hamcrest.Matchers.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest -@AutoConfigureMockMvc -class AdminControllerTest { - - @Autowired private MockMvc mvc; - @Autowired private UserStore userStore; - - @BeforeEach - void resetStore() { - userStore.clear(); - } - - private Cookie registerAndGetToken(String username) throws Exception { - var result = mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "%s", "password": "password123"} - """.formatted(username))) - .andReturn(); - return result.getResponse().getCookie("token"); - } - - @Test - void listUsers_asAdmin_returnsAllUsers() throws Exception { - Cookie adminToken = registerAndGetToken("admin_user"); // first = admin - registerAndGetToken("regular_user"); // second = user - - mvc.perform(get("/api/admin/users").cookie(adminToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[*].username", containsInAnyOrder("admin_user", "regular_user"))); - } - - @Test - void listUsers_asRegularUser_returns403() throws Exception { - registerAndGetToken("admin_user"); // first = admin - Cookie userToken = registerAndGetToken("regular_user"); // second = user - - mvc.perform(get("/api/admin/users").cookie(userToken)) - .andExpect(status().isForbidden()) - .andExpect(jsonPath("$.error").value("Forbidden")); - } - - @Test - void listUsers_unauthenticated_returns401() throws Exception { - mvc.perform(get("/api/admin/users")) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.error").value("Not authenticated")); - } -} diff --git a/src/api/src/test/java/com/spec2cloud/api/AuthControllerTest.java b/src/api/src/test/java/com/spec2cloud/api/AuthControllerTest.java deleted file mode 100644 index 27177cc..0000000 --- a/src/api/src/test/java/com/spec2cloud/api/AuthControllerTest.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.spec2cloud.api; - -import com.spec2cloud.api.service.UserStore; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import jakarta.servlet.http.Cookie; - -import static org.hamcrest.Matchers.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest -@AutoConfigureMockMvc -class AuthControllerTest { - - @Autowired private MockMvc mvc; - @Autowired private ObjectMapper mapper; - @Autowired private UserStore userStore; - - @BeforeEach - void resetStore() { - userStore.clear(); - } - - // ---- Register ---- - - @Test - void register_happyPath_returns201AndCookie() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "password123"} - """)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.message").value("Registration successful")) - .andExpect(cookie().exists("token")) - .andExpect(cookie().httpOnly("token", true)); - } - - @Test - void register_firstUserIsAdmin() throws Exception { - // First user - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "admin_user", "password": "password123"} - """)) - .andExpect(status().isCreated()); - - // Login and check role via /me - var loginResult = mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "admin_user", "password": "password123"} - """)) - .andExpect(status().isOk()) - .andReturn(); - - Cookie tokenCookie = loginResult.getResponse().getCookie("token"); - - mvc.perform(get("/api/auth/me").cookie(tokenCookie)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.role").value("admin")); - } - - @Test - void register_secondUserIsRegularUser() throws Exception { - // First user (admin) - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "first", "password": "password123"} - """)) - .andExpect(status().isCreated()); - - // Second user (regular) - var regResult = mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "second", "password": "password123"} - """)) - .andExpect(status().isCreated()) - .andReturn(); - - Cookie tokenCookie = regResult.getResponse().getCookie("token"); - - mvc.perform(get("/api/auth/me").cookie(tokenCookie)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.role").value("user")); - } - - @Test - void register_duplicateUsername_returns409() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "password123"} - """)) - .andExpect(status().isCreated()); - - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "otherpassword"} - """)) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.error").value("Username already exists")); - } - - @Test - void register_invalidUsername_returns400() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "ab", "password": "password123"} - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.error", containsString("Username must be"))); - } - - @Test - void register_shortPassword_returns400() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "validuser", "password": "short"} - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.error", containsString("Password must be"))); - } - - @Test - void register_missingFields_returns400() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "", "password": ""} - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.error").value("Username and password are required")); - } - - // ---- Login ---- - - @Test - void login_happyPath_returns200AndCookie() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "password123"} - """)); - - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "password123"} - """)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Login successful")) - .andExpect(cookie().exists("token")); - } - - @Test - void login_wrongPassword_returns401() throws Exception { - mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "password123"} - """)); - - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "wrong"} - """)) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.error").value("Invalid username or password")); - } - - @Test - void login_nonexistentUser_returns401() throws Exception { - mvc.perform(post("/api/auth/login") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "nobody", "password": "password123"} - """)) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.error").value("Invalid username or password")); - } - - // ---- Logout ---- - - @Test - void logout_clearsCookie() throws Exception { - mvc.perform(post("/api/auth/logout")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Logged out successfully")) - .andExpect(cookie().maxAge("token", 0)); - } - - // ---- Me ---- - - @Test - void me_authenticated_returnsUserInfo() throws Exception { - var regResult = mvc.perform(post("/api/auth/register") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - {"username": "alice", "password": "password123"} - """)) - .andReturn(); - - Cookie tokenCookie = regResult.getResponse().getCookie("token"); - - mvc.perform(get("/api/auth/me").cookie(tokenCookie)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.username").value("alice")) - .andExpect(jsonPath("$.role").exists()) - .andExpect(jsonPath("$.createdAt").exists()); - } - - @Test - void me_unauthenticated_returns401() throws Exception { - mvc.perform(get("/api/auth/me")) - .andExpect(status().isUnauthorized()) - .andExpect(jsonPath("$.error").value("Not authenticated")); - } -} diff --git a/src/web/src/app/admin/page.tsx b/src/web/src/app/admin/page.tsx deleted file mode 100644 index e95c632..0000000 --- a/src/web/src/app/admin/page.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; - -interface User { - username: string; - role: string; - createdAt: string; -} - -function formatDate(dateStr: string): string { - return new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }).format(new Date(dateStr)); -} - -export default function AdminPage() { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [denied, setDenied] = useState(false); - const router = useRouter(); - - useEffect(() => { - fetch('/api/auth/me') - .then((res) => { - if (res.status === 401) { - router.push('/login'); - return null; - } - if (!res.ok) throw new Error('Failed'); - return res.json(); - }) - .then((data) => { - if (!data) return; - if (data.role !== 'admin') { - setDenied(true); - setLoading(false); - return; - } - return fetch('/api/admin/users') - .then((res) => { - if (!res.ok) throw new Error('Failed'); - return res.json(); - }) - .then((userList) => setUsers(userList)); - }) - .catch(() => setDenied(true)) - .finally(() => setLoading(false)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (loading) { - return ( -
-

Loading users...

-
- ); - } - - if (denied) { - return ( -
-

Access Denied

-

You do not have permission to view this page.

-
- ); - } - - return ( -
-

Users

- - - - - - - - - - {users.map((u) => ( - - - - - - ))} - -
UsernameRoleMember Since
{u.username}{u.role}{formatDate(u.createdAt)}
-
- ); -} diff --git a/src/web/src/app/components/LoginForm.tsx b/src/web/src/app/components/LoginForm.tsx deleted file mode 100644 index 45e6318..0000000 --- a/src/web/src/app/components/LoginForm.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client'; - -import { useState, FormEvent } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import Link from 'next/link'; - -export default function LoginForm() { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const router = useRouter(); - const searchParams = useSearchParams(); - const registered = searchParams.get('registered') === 'true'; - - async function handleSubmit(e: FormEvent) { - e.preventDefault(); - setError(''); - - try { - const res = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - - if (res.ok) { - router.push('/profile'); - } else { - const data = await res.json().catch(() => ({})); - setError(data.error || 'Invalid username or password'); - } - } catch { - setError('An error occurred. Please try again.'); - } - } - - return ( -
-

Log in

- - {registered && ( -

- Registration successful. Please log in. -

- )} - - {error && ( -

{error}

- )} - -
-
- - setUsername(e.target.value)} - className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 text-gray-900" - required - /> -
-
- - setPassword(e.target.value)} - className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 text-gray-900" - required - /> -
- -
- -

- Don't have an account?{' '} - - Register - -

-
- ); -} diff --git a/src/web/src/app/components/NavBar.tsx b/src/web/src/app/components/NavBar.tsx deleted file mode 100644 index c6a177c..0000000 --- a/src/web/src/app/components/NavBar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useAuth } from '../hooks/useAuth'; - -export default function NavBar() { - const { user, loading } = useAuth(); - const router = useRouter(); - - async function handleLogout() { - await fetch('/api/auth/logout', { method: 'POST' }); - router.push('/login'); - router.refresh(); - } - - return ( - - ); -} diff --git a/src/web/src/app/components/RegisterForm.tsx b/src/web/src/app/components/RegisterForm.tsx deleted file mode 100644 index 3c8ad0b..0000000 --- a/src/web/src/app/components/RegisterForm.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client'; - -import { useState, FormEvent } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; - -export default function RegisterForm() { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [confirm, setConfirm] = useState(''); - const [error, setError] = useState(''); - const router = useRouter(); - - async function handleSubmit(e: FormEvent) { - e.preventDefault(); - setError(''); - - if (password !== confirm) { - setError('Passwords do not match.'); - return; - } - - try { - const res = await fetch('/api/auth/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username, password }), - }); - - if (res.status === 201) { - router.push('/login?registered=true'); - } else { - const data = await res.json().catch(() => ({})); - setError(data.error || 'Registration failed. Please try again.'); - } - } catch { - setError('An error occurred. Please try again.'); - } - } - - return ( -
-

Register

- - {error && ( -

{error}

- )} - -
-
- - setUsername(e.target.value)} - className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 text-gray-900" - required - /> -
-
- - setPassword(e.target.value)} - className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 text-gray-900" - required - /> -
-
- - setConfirm(e.target.value)} - className="mt-1 block w-full rounded border border-gray-300 px-3 py-2 text-gray-900" - required - /> -
- -
- -

- Already have an account?{' '} - - Log in - -

-
- ); -} diff --git a/src/web/src/app/globals.css b/src/web/src/app/globals.css deleted file mode 100644 index a2dc41e..0000000 --- a/src/web/src/app/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/web/src/app/hooks/useAuth.ts b/src/web/src/app/hooks/useAuth.ts deleted file mode 100644 index 34f306a..0000000 --- a/src/web/src/app/hooks/useAuth.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -interface User { - username: string; - role: string; - createdAt: string; -} - -export function useAuth() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - useEffect(() => { - fetch('/api/auth/me') - .then((res) => { - if (res.ok) return res.json(); - return null; - }) - .then((data) => setUser(data ?? null)) - .catch(() => setError(true)) - .finally(() => setLoading(false)); - }, []); - - return { user, loading, error }; -} diff --git a/src/web/src/app/layout.tsx b/src/web/src/app/layout.tsx deleted file mode 100644 index b1a5cdd..0000000 --- a/src/web/src/app/layout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; -import NavBar from "./components/NavBar"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "UserAuth", - description: "A secure user authentication demo application.", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - - {children} - - - ); -} diff --git a/src/web/src/app/login/page.tsx b/src/web/src/app/login/page.tsx deleted file mode 100644 index 38c2fe7..0000000 --- a/src/web/src/app/login/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Suspense } from 'react'; -import LoginForm from '../components/LoginForm'; - -export default function LoginPage() { - return ( -
- - - -
- ); -} diff --git a/src/web/src/app/page.tsx b/src/web/src/app/page.tsx deleted file mode 100644 index 9b8e879..0000000 --- a/src/web/src/app/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { useAuth } from './hooks/useAuth'; - -export default function Home() { - const { user, loading } = useAuth(); - - return ( -
-

UserAuth

-

- A simple authentication demo application. -

- {!loading && user && ( - - Go to Profile - - )} - {!loading && !user && ( -
- - Login - - - Register - -
- )} -
- ); -} diff --git a/src/web/src/app/profile/page.tsx b/src/web/src/app/profile/page.tsx deleted file mode 100644 index 18bb221..0000000 --- a/src/web/src/app/profile/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; - -interface User { - username: string; - role: string; - createdAt: string; -} - -function formatDate(dateStr: string): string { - return new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }).format(new Date(dateStr)); -} - -export default function ProfilePage() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - const router = useRouter(); - - function fetchProfile() { - setLoading(true); - setError(false); - fetch('/api/auth/me') - .then((res) => { - if (res.status === 401) { - router.push('/login'); - return null; - } - if (!res.ok) throw new Error('Failed to load'); - return res.json(); - }) - .then((data) => { - if (data) setUser(data); - }) - .catch(() => setError(true)) - .finally(() => setLoading(false)); - } - - useEffect(() => { - fetchProfile(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (loading) { - return ( -
-

Loading profile…

-
- ); - } - - if (error) { - return ( -
-

Failed to load profile. Please try again.

- -
- ); - } - - if (!user) return null; - - return ( -
-
-

{user.username}

- - {user.role} - -

- Member since {formatDate(user.createdAt)} -

-

Use the Logout button in the navigation bar to sign out.

-
-
- ); -} diff --git a/src/web/src/app/register/page.tsx b/src/web/src/app/register/page.tsx deleted file mode 100644 index 2ee4036..0000000 --- a/src/web/src/app/register/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import RegisterForm from '../components/RegisterForm'; - -export default function RegisterPage() { - return ( -
- -
- ); -} diff --git a/tests/features/auth.feature b/tests/features/auth.feature deleted file mode 100644 index 8bd1789..0000000 --- a/tests/features/auth.feature +++ /dev/null @@ -1,68 +0,0 @@ -Feature: Authentication - As a user of the UserAuth application - I want to register, log in, and log out - So that I can securely access my account - - Scenario: Register with valid credentials - Given the user store is empty - When I register with username "alice" and password "Secret12!" - Then the response status should be 201 - And the response should contain "registered" - And the response should contain role "admin" - - Scenario: Register a second user gets user role - Given the user store is empty - And a user "alice" with password "Secret12!" is registered - When I register with username "bob" and password "Secret34!" - Then the response status should be 201 - And the response should contain role "user" - - Scenario: Register with duplicate username returns error - Given the user store is empty - And a user "alice" with password "Secret12!" is registered - When I register with username "alice" and password "Other123!" - Then the response status should be 409 - And the response should contain error "already exists" - - Scenario: Register with missing username returns error - Given the user store is empty - When I register with username "" and password "Secret12!" - Then the response status should be 422 - - Scenario: Register with short password returns error - Given the user store is empty - When I register with username "alice" and password "short" - Then the response status should be 422 - - Scenario: Login with valid credentials - Given the user store is empty - And a user "alice" with password "Secret12!" is registered - When I login with username "alice" and password "Secret12!" - Then the response status should be 200 - And the response should contain "Login successful" - And the response should set a token cookie - - Scenario: Login with invalid password returns error - Given the user store is empty - And a user "alice" with password "Secret12!" is registered - When I login with username "alice" and password "WrongPass1!" - Then the response status should be 401 - And the response should contain error "Invalid" - - Scenario: Login with nonexistent user returns error - Given the user store is empty - When I login with username "ghost" and password "Secret12!" - Then the response status should be 401 - - Scenario: Logout clears the session - Given the user store is empty - And a user "alice" with password "Secret12!" is registered - And I am logged in as "alice" with password "Secret12!" - When I call the logout endpoint - Then the response status should be 200 - And the token cookie should be cleared - - Scenario: Access protected route without authentication returns 401 - Given the user store is empty - When I request my profile without authentication - Then the response status should be 401 diff --git a/tests/features/profile.feature b/tests/features/profile.feature deleted file mode 100644 index a3ad823..0000000 --- a/tests/features/profile.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: User Profile - As an authenticated user - I want to view my profile - So that I can see my account information - - Scenario: View own profile when authenticated - Given the user store is empty - And a user "alice" with password "Secret12!" is registered - And I am logged in as "alice" with password "Secret12!" - When I request my profile - Then the response status should be 200 - And the profile username should be "alice" - And the profile should include a role - And the profile should include a createdAt timestamp - - Scenario: Cannot view profile when not authenticated - Given the user store is empty - When I request my profile without authentication - Then the response status should be 401 diff --git a/tests/features/rbac.feature b/tests/features/rbac.feature deleted file mode 100644 index 69b4d54..0000000 --- a/tests/features/rbac.feature +++ /dev/null @@ -1,33 +0,0 @@ -Feature: Role-Based Access Control - As an admin user - I want to manage other users - And as a regular user I should not have admin access - - Scenario: First registered user becomes admin - Given the user store is empty - When I register with username "firstuser" and password "Secret12!" - Then the response status should be 201 - And the response should contain role "admin" - - Scenario: Admin can list all users - Given the user store is empty - And a user "admin1" with password "Secret12!" is registered - And a user "regular1" with password "Secret34!" is registered - And I am logged in as "admin1" with password "Secret12!" - When I request the admin user list - Then the response status should be 200 - And the user list should contain "admin1" - And the user list should contain "regular1" - - Scenario: Non-admin gets 403 on admin endpoint - Given the user store is empty - And a user "admin1" with password "Secret12!" is registered - And a user "regular1" with password "Secret34!" is registered - And I am logged in as "regular1" with password "Secret34!" - When I request the admin user list - Then the response status should be 403 - - Scenario: Unauthenticated request to admin endpoint returns 401 - Given the user store is empty - When I request the admin user list without authentication - Then the response status should be 401 From 29d639c37d226011a4e4fbdcd3e4a440148c2377 Mon Sep 17 00:00:00 2001 From: Kosta Petan Date: Mon, 30 Mar 2026 09:41:16 +0200 Subject: [PATCH 2/4] feat: add minimal working skeleton (health endpoint + landing page) Bare minimum implementation to verify the stack works with Aspire. No auth, no users, no just a /health endpoint and a landing page.features Specs, features, and implementation will be generated by the spec2cloud orchestrator from user PRDs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/com/spec2cloud/api/Application.java | 12 ++++++++++++ .../api/controller/HealthController.java | 19 +++++++++++++++++++ .../src/main/resources/application.properties | 1 + src/web/src/app/globals.css | 1 + src/web/src/app/layout.tsx | 19 +++++++++++++++++++ src/web/src/app/page.tsx | 10 ++++++++++ 6 files changed, 62 insertions(+) create mode 100644 src/api/src/main/java/com/spec2cloud/api/Application.java create mode 100644 src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java create mode 100644 src/api/src/main/resources/application.properties create mode 100644 src/web/src/app/globals.css create mode 100644 src/web/src/app/layout.tsx create mode 100644 src/web/src/app/page.tsx diff --git a/src/api/src/main/java/com/spec2cloud/api/Application.java b/src/api/src/main/java/com/spec2cloud/api/Application.java new file mode 100644 index 0000000..03bf2bf --- /dev/null +++ b/src/api/src/main/java/com/spec2cloud/api/Application.java @@ -0,0 +1,12 @@ +package com.spec2cloud.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java b/src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java new file mode 100644 index 0000000..3879dd9 --- /dev/null +++ b/src/api/src/main/java/com/spec2cloud/api/controller/HealthController.java @@ -0,0 +1,19 @@ +package com.spec2cloud.api.controller; + +import java.time.Instant; +import java.util.Map; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthController { + + @GetMapping("/health") + public Map health() { + return Map.of( + "status", "healthy", + "timestamp", Instant.now().toString() + ); + } +} diff --git a/src/api/src/main/resources/application.properties b/src/api/src/main/resources/application.properties new file mode 100644 index 0000000..4c00e40 --- /dev/null +++ b/src/api/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8080 diff --git a/src/web/src/app/globals.css b/src/web/src/app/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/src/web/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/web/src/app/layout.tsx b/src/web/src/app/layout.tsx new file mode 100644 index 0000000..79aa05b --- /dev/null +++ b/src/web/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "spec2cloud — Java", + description: "spec2cloud shell — Java", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/web/src/app/page.tsx b/src/web/src/app/page.tsx new file mode 100644 index 0000000..18fb0b4 --- /dev/null +++ b/src/web/src/app/page.tsx @@ -0,0 +1,10 @@ +export default function Home() { + return ( +
+

spec2cloud shell — Java

+

+ This shell is ready for spec2cloud +

+
+ ); +} From 455ada56eb6c19940fcfbc4c13951c0466ef0906 Mon Sep 17 00:00:00 2001 From: Kosta Petan Date: Mon, 30 Mar 2026 16:31:30 +0200 Subject: [PATCH 3/4] feat: enhance UI/UX design phase with 6 mandatory artifacts Synced from spec2cloud vNext: - AGENTS.md: Phase 1b now produces 6 structured artifacts in specs/ui/ (screen-map, design-system, prototypes, component-inventory, flow-walkthrough, walkthrough HTML) with clear downstream consumers (E2E, Gherkin, Implementation) - AGENTS.md: Added specs/ui/ subtree to project structure - copilot-instructions.md: Added specs/ui/ to file organization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 1 + AGENTS.md | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 90720b7..da2a09d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -34,6 +34,7 @@ is driven by automated tests generated from those specifications. ``` specs/ → Specifications (PRD, FRDs, Gherkin) specs/contracts/→ Contracts (API specs per feature, infrastructure resources) +specs/ui/ → UI/UX design artifacts (screen-map, design-system, component-inventory, prototypes/, walkthroughs) specs/tech-stack.md → Resolved tech stack (all technology decisions, wiring, deployment) e2e/ → Playwright end-to-end tests (integration slice) tests/ → Cucumber.js BDD tests (integration slice) diff --git a/AGENTS.md b/AGENTS.md index ee1df23..8fe4ba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ All specialized logic lives in `.github/skills/` following the [agentskills.io]( | Phase | Skill | Purpose | |-------|-------|---------| | 1a | `spec-refinement` | PRD/FRD review through product + technical lenses | -| 1b | `ui-ux-design` | FRD → interactive HTML wireframe prototypes | +| 1b | `ui-ux-design` | FRD → screen map, design system, HTML prototypes, component inventory, walkthroughs | | 1c | *(orchestrator)* | Increment planning (inline — no dedicated skill) | | 1d | `tech-stack-resolution` | Inventory, research, resolve all technologies | @@ -149,8 +149,20 @@ Review PRD/FRDs through product + technical lenses (max 5 passes). Break PRD int **Exit:** Human approves all FRDs. **Human gate:** Yes. #### 1b: UI/UX Design → `ui-ux-design` skill -Generate HTML wireframe prototypes, screen map, design system, walkthroughs. Serve via HTTP for review. -**Exit:** Human approves all UI/UX artifacts. **Human gate:** Yes. +Follow the skill’s 8-step process (Screen Inventory → Design System → HTML Prototypes → Component Inventory → Serve → Walkthrough → Review Loop → Cleanup). The skill produces **6 mandatory artifacts** — all must exist before the phase can exit: + +| Artifact | Path | Consumed By | +|----------|------|-------------| +| Screen map | `specs/ui/screen-map.md` | E2E Generation (POM structure), Gherkin (screen names) | +| Design system | `specs/ui/design-system.md` | Implementation Web slice (design tokens) | +| HTML prototypes | `specs/ui/prototypes/*.html` + `index.html` | E2E Generation (POM selectors from `data-testid`), Implementation (visual spec) | +| Component inventory | `specs/ui/component-inventory.md` | E2E Generation (POM selectors), Gherkin (scenario vocabulary), Implementation (component structure) | +| Flow walkthrough | `specs/ui/flow-walkthrough.md` | E2E Generation (e2e test flows — **source of truth**), Gherkin (scenario context) | +| Walkthrough HTML | `specs/ui/walkthrough.html` | Docs site (embedded walkthrough) | + +> **⚠ Do NOT produce a single monolithic wireframe file.** Each screen must be a separate HTML file in `specs/ui/prototypes/`. The supporting markdown artifacts (screen-map, design-system, component-inventory, flow-walkthrough) are equally important — they drive downstream Gherkin, E2E, and implementation phases. + +**Exit:** Human approves **all 6 artifacts** after browsing the served prototypes. **Human gate:** Yes. #### 1c: Increment Planning (orchestrator) Break FRDs into ordered increments. Walking skeleton first, then by dependency chain. From 2e0eb1e0aa62ece7b6be1cbe90d6d75ebb66cef6 Mon Sep 17 00:00:00 2001 From: Kosta Petan Date: Mon, 30 Mar 2026 22:34:41 +0200 Subject: [PATCH 4/4] feat: sync with spec2cloud completion checklists, test discipline, state schemavNext Synced from spec2cloud vNext (4 commits): - All 45 skills updated with mandatory completion checklists and blocking conditions - AGENTS.md: new Section 9 (Test Discipline Gospel), enhanced Phase 1b/2 descriptions, updated state.json schema documentation - copilot-instructions.md: bug-spot protocol, specs/ui/ in file organization - .spec2cloud/state.json: enhanced schema with new phase tracking fields - docs/: updated conceptual docs (greenfield, brownfield, concepts, architecture, etc.) - templates/shell/apphost.cs: updated web and docs service config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 51 +++-- .github/skills/api-extractor/SKILL.md | 12 ++ .github/skills/architecture-mapper/SKILL.md | 12 ++ .github/skills/audit-log/SKILL.md | 12 ++ .github/skills/azure-deployment/SKILL.md | 14 ++ .github/skills/bug-fix/SKILL.md | 15 ++ .github/skills/build-check/SKILL.md | 11 + .../skills/cloud-native-assessment/SKILL.md | 13 ++ .github/skills/cloud-native-planner/SKILL.md | 13 ++ .github/skills/codebase-scanner/SKILL.md | 12 ++ .github/skills/commit-protocol/SKILL.md | 12 ++ .github/skills/contract-generation/SKILL.md | 12 ++ .github/skills/data-model-extractor/SKILL.md | 12 ++ .github/skills/dependency-inventory/SKILL.md | 12 ++ .github/skills/e2e-generation/SKILL.md | 14 ++ .github/skills/extension-planner/SKILL.md | 13 ++ .github/skills/frd-generator/SKILL.md | 2 + .github/skills/gherkin-generation/SKILL.md | 15 ++ .github/skills/implementation/SKILL.md | 14 ++ .../skills/modernization-assessment/SKILL.md | 12 ++ .github/skills/modernization-planner/SKILL.md | 14 ++ .../skills/performance-assessment/SKILL.md | 13 ++ .github/skills/prd-generator/SKILL.md | 2 + .github/skills/resume/SKILL.md | 13 ++ .github/skills/rewrite-assessment/SKILL.md | 13 ++ .github/skills/rewrite-planner/SKILL.md | 14 ++ .github/skills/security-assessment/SKILL.md | 14 ++ .github/skills/security-planner/SKILL.md | 13 ++ .github/skills/state-management/SKILL.md | 13 ++ .github/skills/tech-stack-resolution/SKILL.md | 14 ++ .github/skills/test-discovery/SKILL.md | 12 ++ .github/skills/test-generation/SKILL.md | 20 +- .github/skills/test-runner/SKILL.md | 12 ++ .github/skills/ui-ux-design/SKILL.md | 2 +- .spec2cloud/state.json | 19 +- AGENTS.md | 196 +++++++++++++++-- docs/README.md | 2 +- docs/architecture.md | 2 +- docs/brownfield.md | 8 +- docs/concepts.md | 4 +- docs/examples/greenfield-walkthrough.md | 204 ++++++++++++++---- docs/greenfield.md | 13 ++ docs/shells.md | 50 +++-- docs/skills.md | 6 +- docs/state-and-gates.md | 8 +- 45 files changed, 850 insertions(+), 114 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index da2a09d..dc76aad 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -15,7 +15,7 @@ is driven by automated tests generated from those specifications. ## Coding Conventions - Write minimal code to pass tests — no gold-plating - Follow existing patterns in the codebase -- All code must be covered by tests (unit, integration, or e2e) +- All code must be covered by tests (unit, integration, or e2e). **Exception:** Brownfield Track B features use manual verification checklists when automated tests are not feasible (see AGENTS.md Testability Gate). - No hardcoded secrets — use environment variables - No hardcoded URLs — use configuration - Error handling: every external call must have error handling @@ -25,16 +25,41 @@ is driven by automated tests generated from those specifications. ## Spec-Driven Development Rules - **Never implement features not in the specs** (specs/frd-*.md) - **Never modify tests** without human approval — tests are the contract -- **Always check Gherkin scenarios** before implementing a feature - **Never skip tests** — no `test.skip()`, no `xit()`, no `@pending`, no commenting out. Tests are proof that specs are met. +- **Always check Gherkin scenarios** before implementing a feature - **Always run tests** after making changes — the FULL suite, not just "relevant" tests - If a spec seems wrong, flag it — do not silently deviate +## Test Discipline Gospel +Tests are the **proof** that specifications have been implemented correctly. They are not optional, not skippable, and not negotiable. See AGENTS.md §9 for the full protocol. + +Key rules: +- **Tests = proof of spec completion.** A feature without passing tests is not done. +- **Phase order is sacred.** Tests → Contracts → Implementation → Verify. Never skip or reorder. +- **All tests must pass before advancing.** No deployment, no commit to main while any test fails. +- **Regression is mandatory.** After every change, run ALL tests — not just new ones. + +### When the User Spots a Problem +At any point during the flow, if the user identifies something wrong: +1. **Pause** current work +2. **Offer** to generate a test that captures the problem +3. **Present** the test for user approval (mandatory human gate) +4. **Fix** minimally after approval +5. **Verify** the test passes and full regression is green +6. **Resume** the interrupted work + +This is the **bug-spot protocol** — it applies in every phase. The user's approval of the test is mandatory because the test defines what "fixed" means. + ## File Organization ``` specs/ → Specifications (PRD, FRDs, Gherkin) specs/contracts/→ Contracts (API specs per feature, infrastructure resources) specs/ui/ → UI/UX design artifacts (screen-map, design-system, component-inventory, prototypes/, walkthroughs) +specs/features/ → Gherkin feature files (greenfield + brownfield Track A) +specs/adrs/ → Architecture Decision Records +specs/docs/ → Brownfield extraction outputs (technology/, architecture/, testing/) +specs/assessment/ → Assessment outputs (modernization, security, cloud-native, etc.) +specs/increment-plan.md → Ordered increment delivery plan specs/tech-stack.md → Resolved tech stack (all technology decisions, wiring, deployment) e2e/ → Playwright end-to-end tests (integration slice) tests/ → Cucumber.js BDD tests (integration slice) @@ -67,9 +92,11 @@ The agent MUST pause and ask for human approval at these points: - After UI/UX design (before increment planning) - After increment plan approval (before tech stack resolution) - After tech stack resolution (before first increment) — technology choices must be approved -- Per increment: after Gherkin generation (before BDD test scaffolding) +- Per increment: after Gherkin generation (before BDD test scaffolding) — **user approves scenarios** +- Per increment: after test generation (before contracts) — **user approves test code** - Per increment: after implementation (before deployment) — via PR review - Per increment: after deployment (verify it works) +- **Bug-spot protocol: user must approve the bug-reproducing test before any fix proceeds** ## State Management - Read `.spec2cloud/state.json` at the start of every session @@ -140,25 +167,11 @@ specs/ ## Bug Fix Protocol - Bug fixes use the `bug-fix` skill — lightweight path through the pipeline -- Every bug fix MUST: link to an FRD, create a failing test, fix minimally, verify regression +- Every bug fix MUST: link to an FRD, create a failing test, **get user approval on the test**, fix minimally, verify regression +- The user approves the test because it defines what "fixed" means — this gate is not optional - Commit format: `[bugfix] {frd-id}: {description}` - Tracked as micro-increments in state.json - -## Test Discipline Gospel -Tests are the **proof** that specifications have been implemented correctly. They are not optional, not skippable, and not negotiable. - -Key rules: -- **Tests = proof of spec completion.** A feature without passing tests is not done. -- **Phase order is sacred.** Tests → Contracts → Implementation → Verify. Never skip or reorder. -- **All tests must pass before advancing.** No deployment, no commit to main while any test fails. -- **Regression is mandatory.** After every change, run ALL tests — not just new ones. - -### When the User Spots a Problem -At any point during the flow, if the user identifies something wrong: -1. **Pause** current work -2. **Offer** to generate a test that captures the problem - ## Shell-Specific Extensions diff --git a/.github/skills/api-extractor/SKILL.md b/.github/skills/api-extractor/SKILL.md index f627fe4..68f6623 100644 --- a/.github/skills/api-extractor/SKILL.md +++ b/.github/skills/api-extractor/SKILL.md @@ -228,3 +228,15 @@ sharedTypes: Flask, etc.) in the `path` field. 7. **Every endpoint.** Missing a route that exists in code is a failure. Scan systematically — do not rely on sampling. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking api-extractor as complete: + +- [ ] At least one contract file exists in `specs/contracts/api/` in OpenAPI-compatible YAML format +- [ ] Every HTTP route/endpoint found in source code is documented (method, path, parameters) +- [ ] Request and response schemas are documented where determinable; noted as "not determined" otherwise +- [ ] Authentication/authorization patterns are identified per endpoint (e.g., JWT, API key, public) +- [ ] Error response shapes are documented where the code defines them + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to the next extraction step. diff --git a/.github/skills/architecture-mapper/SKILL.md b/.github/skills/architecture-mapper/SKILL.md index 2aed249..2cf266c 100644 --- a/.github/skills/architecture-mapper/SKILL.md +++ b/.github/skills/architecture-mapper/SKILL.md @@ -245,3 +245,15 @@ _Extracted on [date]._ external API, queue) is a failure. Trace every outbound connection. 7. **Ambiguity is okay.** If you cannot determine a pattern or relationship from the code, say "not determinable" rather than guessing. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking architecture-mapper as complete: + +- [ ] `specs/docs/architecture/overview.md` exists with a high-level Mermaid component diagram +- [ ] `specs/docs/architecture/components.md` exists with per-component detail (responsibility, dependencies, interfaces) +- [ ] All integration points are cataloged (databases, external APIs, queues, caches, file systems) +- [ ] Data flow between components is documented with a Mermaid sequence or flow diagram +- [ ] Layer boundaries (if any) are identified (e.g., controller → service → repository) + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to the next extraction step. diff --git a/.github/skills/audit-log/SKILL.md b/.github/skills/audit-log/SKILL.md index db3c402..52da25b 100644 --- a/.github/skills/audit-log/SKILL.md +++ b/.github/skills/audit-log/SKILL.md @@ -39,3 +39,15 @@ Append every significant action to `.spec2cloud/audit.log`. Never overwrite — ``` [2026-02-09T14:40:00Z] phase=deployment action=azd-provision result=error message="quota exceeded in eastus" ``` + +## Mandatory Completion Checklist + +Every audit log entry MUST contain ALL of the following fields: + +- [ ] ISO-8601 timestamp (e.g., `2026-02-09T14:40:00Z`) +- [ ] Context identifier: `phase=` or `increment=` (what work is being done) +- [ ] `action=` field (what specific action was taken) +- [ ] `result=` field (outcome: `done`, `error`, `approved`, `rejected`, etc.) +- [ ] `message=` field for errors and rejections (human-readable detail) + +**BLOCKING**: An audit entry missing any required field makes the audit trail incomplete. The orchestrator must ensure every append follows this schema. diff --git a/.github/skills/azure-deployment/SKILL.md b/.github/skills/azure-deployment/SKILL.md index 085eaa3..649671c 100644 --- a/.github/skills/azure-deployment/SKILL.md +++ b/.github/skills/azure-deployment/SKILL.md @@ -146,3 +146,17 @@ If smoke tests fail after deployment: See `references/stack-deployment.md` for AZD service structure, Container Apps configuration, Dockerfile details, infrastructure templates, environment variables, and smoke test commands. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking azure-deployment as complete: + +- [ ] Full regression suite passes locally before deployment +- [ ] `azd provision` completes without errors +- [ ] `azd deploy` completes without errors +- [ ] Smoke tests pass against the deployed endpoints (not localhost) +- [ ] HTTPS is enabled on all deployed endpoints +- [ ] Application logs show no startup errors (check via `azd monitor`) +- [ ] State JSON and audit log are updated with deployment URLs and status + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before marking the increment as delivered. diff --git a/.github/skills/bug-fix/SKILL.md b/.github/skills/bug-fix/SKILL.md index 5b1d9cb..0add567 100644 --- a/.github/skills/bug-fix/SKILL.md +++ b/.github/skills/bug-fix/SKILL.md @@ -216,3 +216,18 @@ Append to `.spec2cloud/audit.log`: - **Minimal change only.** Refactoring goes in separate increments. - **No silent fixes.** Every fix is committed, logged, and tracked in state. - **Regression must pass.** If the fix breaks other tests, the fix is wrong. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking bug-fix as complete: + +- [ ] Bug is linked to a specific FRD (or flagged as a feature request if no FRD exists) +- [ ] A failing test exists that reproduces the bug +- [ ] Human approved the reproduction test (mandatory gate) +- [ ] The fix is minimal (< 50 lines; larger fixes escalated to full increments) +- [ ] The reproduction test now passes +- [ ] Full regression suite passes (no other tests broken) +- [ ] State JSON records the micro-increment; audit log has the fix entry +- [ ] Commit uses `[bugfix] {frd-id}: {description}` format + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items. diff --git a/.github/skills/build-check/SKILL.md b/.github/skills/build-check/SKILL.md index 9ff80b6..80da4e4 100644 --- a/.github/skills/build-check/SKILL.md +++ b/.github/skills/build-check/SKILL.md @@ -38,3 +38,14 @@ Details: - Web (Next.js) requires `output: 'standalone'` in `next.config.ts` - API uses TypeScript with Express + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking build-check as complete: + +- [ ] Every service (API, Web) has been built — none skipped +- [ ] Each service reports PASS or FAIL with error/warning counts +- [ ] If any service fails to build, the failure details (file, line, message) are included +- [ ] A partial build (one service passes, another not attempted) is reported as "incomplete" — not as PASS + +**BLOCKING**: A build-check with any FAIL or incomplete service means the codebase is not ready for testing or deployment. The orchestrator must fix build errors before proceeding. diff --git a/.github/skills/cloud-native-assessment/SKILL.md b/.github/skills/cloud-native-assessment/SKILL.md index 75f9616..0f59482 100644 --- a/.github/skills/cloud-native-assessment/SKILL.md +++ b/.github/skills/cloud-native-assessment/SKILL.md @@ -161,3 +161,16 @@ Generate ADRs via the `adr` skill when the assessment reveals service selection - Azure service recommendations should consider cost. Don't recommend AKS when Container Apps suffices. - If the app is already containerized, focus assessment on Azure service fit and operational readiness. - Monolith decomposition is out of scope. Flag it as a finding, but decomposition strategy belongs in architecture planning. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking cloud-native-assessment as complete: + +- [ ] `specs/assessment/cloud-native.md` exists with: 12-factor compliance scorecard, containerization readiness, and Azure service fit analysis +- [ ] Each 12-factor principle is rated (compliant / partially compliant / non-compliant) with evidence +- [ ] Azure service recommendations are provided with cost tier awareness (e.g., Container Apps vs AKS) +- [ ] Configuration externalization gaps are identified (hardcoded values, file-based config) +- [ ] At least one ADR exists in `specs/adrs/` for significant platform/service decisions +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to planning. diff --git a/.github/skills/cloud-native-planner/SKILL.md b/.github/skills/cloud-native-planner/SKILL.md index f2cdf3f..415cbf6 100644 --- a/.github/skills/cloud-native-planner/SKILL.md +++ b/.github/skills/cloud-native-planner/SKILL.md @@ -239,3 +239,16 @@ proceeds through the standard Phase 2 pipeline: 2. **Contract generation** — update infra contracts for new Azure resources 3. **Implementation** — build Dockerfiles, IaC, CI/CD workflows 4. **Build & deploy** — verify containerized app builds and deploys to Azure + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking cloud-native-planner as complete: + +- [ ] `specs/increment-plan.md` is updated with all cloud-native increments (unique IDs, scope, dependencies, effort) +- [ ] Infrastructure increments are ordered: containerization → config externalization → IaC → observability → CI/CD +- [ ] Every new Azure resource has a corresponding entry in `specs/contracts/infra/resources.yaml` +- [ ] Security defaults are planned (non-root containers, managed identity, Key Vault for secrets) +- [ ] All increments are consistent with existing ADRs for Azure service choices +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to Phase 2 delivery. diff --git a/.github/skills/codebase-scanner/SKILL.md b/.github/skills/codebase-scanner/SKILL.md index 04e8ee2..3cf2279 100644 --- a/.github/skills/codebase-scanner/SKILL.md +++ b/.github/skills/codebase-scanner/SKILL.md @@ -206,3 +206,15 @@ _Extracted on [date]. This is a factual inventory of the project as it exists._ files (e.g., `"^18.2.0"` not just `"18"`). 7. **No inferencing beyond the code.** If you can't determine something from the files, say "not determinable from source" — do not guess. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking codebase-scanner as complete: + +- [ ] `specs/docs/technology/stack.md` exists and contains: languages, frameworks, package managers, build tools, and entry points +- [ ] Every language detected has version constraints documented (from manifest files) +- [ ] Every framework detected has its role documented (web, API, testing, etc.) +- [ ] Entry points are identified for each application boundary +- [ ] Monorepo workspaces (if any) are inventoried separately + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to the next extraction step. diff --git a/.github/skills/commit-protocol/SKILL.md b/.github/skills/commit-protocol/SKILL.md index d098834..f8ccc0d 100644 --- a/.github/skills/commit-protocol/SKILL.md +++ b/.github/skills/commit-protocol/SKILL.md @@ -40,3 +40,15 @@ Each increment is a self-contained delivery. Commits at increment boundaries mea - `git log --oneline --grep="\[increment\]"` shows the delivery timeline - Each delivered increment is a revertable, deployable unit - Mid-increment commits at slice granularity create resumable checkpoints + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following for every commit: + +- [ ] Commit message follows the format table above (correct prefix for the phase/step) +- [ ] `.spec2cloud/state.json` is included in the commit +- [ ] `.spec2cloud/audit.log` is included in the commit +- [ ] No secrets, `.env` files, or `node_modules` are staged +- [ ] Co-authored-by trailer is present + +**BLOCKING**: A commit without state.json and audit.log breaks resume capability. The orchestrator must include them before finalizing the commit. diff --git a/.github/skills/contract-generation/SKILL.md b/.github/skills/contract-generation/SKILL.md index e0e9eee..016b1a4 100644 --- a/.github/skills/contract-generation/SKILL.md +++ b/.github/skills/contract-generation/SKILL.md @@ -149,3 +149,15 @@ Append to `.spec2cloud/audit.log`: ``` [ISO-timestamp] increment={id} step=contracts action=contracts-generated result=done ``` + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking contract-generation as complete: + +- [ ] API contracts exist in `specs/contracts/api/` for every endpoint in the increment +- [ ] Shared types in `src/shared/types/` cover all data structures used across API and Web +- [ ] `specs/contracts/infra/resources.yaml` is updated with any NEW infrastructure needs for this increment (new Azure resources, new model deployments, new env vars) +- [ ] Any new Azure resources needed are flagged for provisioning and have corresponding Bicep definitions in `infra/` +- [ ] Any new environment variables are documented in both `resources.yaml` (per-resource env_vars) and `specs/tech-stack.md` (per-increment map) + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to implementation. diff --git a/.github/skills/data-model-extractor/SKILL.md b/.github/skills/data-model-extractor/SKILL.md index 603f6eb..8b7912b 100644 --- a/.github/skills/data-model-extractor/SKILL.md +++ b/.github/skills/data-model-extractor/SKILL.md @@ -261,3 +261,15 @@ _Extracted on [date]. Documents the data layer as defined in code._ 8. **Relationship accuracy.** Every foreign key and relationship decorator must appear in the output. Verify by checking both sides of bidirectional relationships. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking data-model-extractor as complete: + +- [ ] `specs/docs/architecture/data-models.md` exists with a Mermaid ERD diagram +- [ ] Every ORM model, migration file, and schema definition in the codebase is covered +- [ ] All entities have their fields, types, and constraints documented +- [ ] All relationships (foreign keys, one-to-many, many-to-many) are documented with both sides verified +- [ ] Index definitions are cataloged where present in schema files + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to the next extraction step. diff --git a/.github/skills/dependency-inventory/SKILL.md b/.github/skills/dependency-inventory/SKILL.md index 965829f..9f60c27 100644 --- a/.github/skills/dependency-inventory/SKILL.md +++ b/.github/skills/dependency-inventory/SKILL.md @@ -208,3 +208,15 @@ _Extracted on [date]. This is a factual catalog of all declared dependencies._ then produce a unified summary. 8. **Transitive accuracy.** Only report transitive relationships you can verify from lock files. Do not guess transitive trees. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking dependency-inventory as complete: + +- [ ] `specs/docs/technology/dependencies.md` exists and contains: all direct dependencies with versions, purposes, and license info +- [ ] Every package manifest file in the project is covered (package.json, requirements.txt, *.csproj, etc.) +- [ ] Lock file versions are used where available; absence of lock file is noted +- [ ] Dependency relationships (which packages depend on which) are documented +- [ ] Monorepo workspaces (if any) have per-workspace inventories plus a unified summary + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to the next extraction step. diff --git a/.github/skills/e2e-generation/SKILL.md b/.github/skills/e2e-generation/SKILL.md index 2c878ef..a79db7d 100644 --- a/.github/skills/e2e-generation/SKILL.md +++ b/.github/skills/e2e-generation/SKILL.md @@ -229,3 +229,17 @@ After completing e2e generation: [TIMESTAMP] e2e-generation: All specs compile and are listed ✅ ``` 3. Commit all generated files with message: `[e2e-gen] scaffold e2e tests for all flows` + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking e2e-generation as complete: + +- [ ] At least one Playwright spec file (`e2e/*.spec.ts`) exists for every user flow in `specs/ui/flow-walkthrough.md` +- [ ] A Page Object Model (`e2e/pages/*.page.ts`) exists for every screen in `specs/ui/screen-map.md` +- [ ] Every POM uses `data-testid` selectors from `specs/ui/component-inventory.md` (not CSS classes or XPath) +- [ ] `e2e/playwright.config.ts` exists and is configured for the Aspire environment +- [ ] All spec files compile successfully (`npx playwright test --list` returns all specs without errors) +- [ ] Navigation flows between pages are tested (not just individual page interactions) +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to Gherkin generation. diff --git a/.github/skills/extension-planner/SKILL.md b/.github/skills/extension-planner/SKILL.md index fbd35e1..3bc9afd 100644 --- a/.github/skills/extension-planner/SKILL.md +++ b/.github/skills/extension-planner/SKILL.md @@ -241,3 +241,16 @@ Before finalizing, verify: After approval at the human gate, each increment proceeds through Phase 2: gherkin → test generation → contract generation → implementation → deploy. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking extension-planner as complete: + +- [ ] New FRDs exist in `specs/frd-*.md` for each extension feature (one FRD per feature) +- [ ] `specs/increment-plan.md` is updated with all extension increments (unique IDs, scope, dependencies, effort) +- [ ] Each FRD documents integration points with existing code +- [ ] Prerequisite increments are verified to not break existing features (backward compatibility) +- [ ] Increment ordering respects dependency chains +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to Phase 2 delivery. diff --git a/.github/skills/frd-generator/SKILL.md b/.github/skills/frd-generator/SKILL.md index 0dc9706..d9ff41a 100644 --- a/.github/skills/frd-generator/SKILL.md +++ b/.github/skills/frd-generator/SKILL.md @@ -431,3 +431,5 @@ Before presenting FRDs for human review: - [ ] **Track B only:** Testability Roadmap lists all blockers with effort estimates - [ ] **Track B only:** Track B sections are omitted if feature is testable (Track A) - [ ] State JSON is updated with all generated FRDs + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing. FRDs drive all downstream Gherkin, test, and implementation work — incomplete FRDs cause cascading gaps. diff --git a/.github/skills/gherkin-generation/SKILL.md b/.github/skills/gherkin-generation/SKILL.md index 4a5d187..78815cb 100644 --- a/.github/skills/gherkin-generation/SKILL.md +++ b/.github/skills/gherkin-generation/SKILL.md @@ -188,3 +188,18 @@ Notice: - Steps use domain language ("submits the login form"), not implementation details. - Data is concrete and realistic. - Scenarios are independent — neither relies on the other's state. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking gherkin-generation as complete: + +- [ ] At least one `.feature` file exists in `specs/features/` for every FRD in scope for this increment +- [ ] Every acceptance criterion in the FRD(s) has at least one corresponding Gherkin scenario +- [ ] Each scenario tests exactly one behavior (no multi-behavior scenarios) +- [ ] Tags are applied: feature tag (`@feature-name`), type tags (`@happy`, `@error`, `@edge`), and track tags (`@existing-behavior` for brownfield capture) +- [ ] Steps use domain language, not implementation details (no CSS selectors, no API paths in step text) +- [ ] Error/edge case scenarios exist for every documented error condition in the FRDs +- [ ] `data-testid` attribute names from `specs/ui/component-inventory.md` are used in UI-related steps (if UI/UX design phase completed) +- [ ] State JSON is updated with generated feature files + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to test generation. diff --git a/.github/skills/implementation/SKILL.md b/.github/skills/implementation/SKILL.md index 6b67ffe..b8bd1ae 100644 --- a/.github/skills/implementation/SKILL.md +++ b/.github/skills/implementation/SKILL.md @@ -228,3 +228,17 @@ If a human edits code while you are in the implementation phase: See `references/stack-patterns.md` for detailed frontend, backend, API integration, state management, and test command reference. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking implementation as complete: + +- [ ] All tests from Step 1 (e2e, Gherkin, unit) pass — zero failures, zero skipped +- [ ] API slice builds and all API-specific tests pass +- [ ] Web slice builds and all Web-specific tests pass +- [ ] Integration slice passes (API + Web wired together via Aspire) +- [ ] Full regression suite passes (not just tests for the current increment) +- [ ] No `test.skip()`, `xit()`, `@pending`, or commented-out tests exist in the codebase +- [ ] State JSON and audit log are updated with per-slice completion + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to deployment. diff --git a/.github/skills/modernization-assessment/SKILL.md b/.github/skills/modernization-assessment/SKILL.md index d59db55..95319f8 100644 --- a/.github/skills/modernization-assessment/SKILL.md +++ b/.github/skills/modernization-assessment/SKILL.md @@ -155,3 +155,15 @@ Each ADR should reference the specific assessment findings that triggered it. - Effort estimates are rough guidance, not commitments. - If extraction outputs are incomplete, note gaps rather than guessing. - Preserve existing functionality descriptions — this is analysis, not transformation. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking modernization-assessment as complete: + +- [ ] `specs/assessment/modernization.md` exists with: executive summary, findings by category, severity ratings, and effort estimates +- [ ] Every finding has a severity level (critical / high / medium / low) and an estimated effort +- [ ] At least one ADR exists in `specs/adrs/` for each significant architecture decision surfaced +- [ ] Findings reference specific files, line numbers, or patterns from the extraction outputs +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to planning. diff --git a/.github/skills/modernization-planner/SKILL.md b/.github/skills/modernization-planner/SKILL.md index 83dd5fc..8407d2d 100644 --- a/.github/skills/modernization-planner/SKILL.md +++ b/.github/skills/modernization-planner/SKILL.md @@ -220,3 +220,17 @@ proceeds through the standard Phase 2 pipeline: 2. **Contract generation** — update contracts if APIs change 3. **Implementation** — execute the modernization 4. **Build & deploy** — verify the app builds and deploys successfully + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking modernization-planner as complete: + +- [ ] `specs/increment-plan.md` is updated with all modernization increments (unique IDs, scope, dependencies, effort) +- [ ] Each increment addresses exactly one concern (runtime upgrade OR library swap OR config migration — not multiple) +- [ ] Increment ordering respects dependency chains (no increment depends on an unplanned predecessor) +- [ ] Gherkin deltas (new/modified scenarios) are specified per increment for Track A features +- [ ] Behavioral doc updates are specified per increment for Track B features +- [ ] All increments are consistent with existing ADRs; conflicts are flagged +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to Phase 2 delivery. diff --git a/.github/skills/performance-assessment/SKILL.md b/.github/skills/performance-assessment/SKILL.md index de6ccdb..3197985 100644 --- a/.github/skills/performance-assessment/SKILL.md +++ b/.github/skills/performance-assessment/SKILL.md @@ -180,3 +180,16 @@ Generate ADRs via the `adr` skill when optimization requires architectural decis - Context matters. An N+1 query on a list that always returns 3 items is low impact. The same pattern on a list with 10,000 items is high impact. Note the data volume context when available. - Frontend and backend performance are different disciplines. Clearly separate findings by layer. - Always suggest measurement before optimization. The roadmap should include "verify with profiling" steps. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking performance-assessment as complete: + +- [ ] `specs/assessment/performance.md` exists with: findings by layer (frontend / backend / database / network), severity ratings, and quick-win identification +- [ ] Every finding has an impact level (high / medium / low) and estimated effort to fix +- [ ] N+1 query patterns, missing indexes, and unoptimized queries are explicitly checked and reported +- [ ] Frontend performance patterns are assessed separately from backend +- [ ] "Measure before optimize" steps are included in the remediation roadmap +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to planning. diff --git a/.github/skills/prd-generator/SKILL.md b/.github/skills/prd-generator/SKILL.md index fafed7c..6b6d2a0 100644 --- a/.github/skills/prd-generator/SKILL.md +++ b/.github/skills/prd-generator/SKILL.md @@ -265,3 +265,5 @@ Before presenting the PRD for human review: - [ ] No fabricated features — every entry is backed by code - [ ] Format matches greenfield PRD template exactly - [ ] State JSON is updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing. The PRD is the foundation for all downstream FRDs — gaps here propagate everywhere. diff --git a/.github/skills/resume/SKILL.md b/.github/skills/resume/SKILL.md index 6a794b1..a9dbca8 100644 --- a/.github/skills/resume/SKILL.md +++ b/.github/skills/resume/SKILL.md @@ -37,3 +37,16 @@ On every CLI session start, check for existing state. - Do not revert human edits — adjust your plan to the new state 5. **Continue the Ralph loop** from the determined position. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before resuming work: + +- [ ] `.spec2cloud/state.json` exists and is valid JSON +- [ ] Current phase and increment are determined from state +- [ ] Re-validation ran for the current step (tests compile, builds pass, deployment status checked — as appropriate) +- [ ] If re-validation results differ from state, state was updated to reflect reality +- [ ] Human edits during pause (if any) are detected and treated as ground truth — not reverted +- [ ] Audit log entry records the resume event with any discrepancies found + +**BLOCKING**: If state.json is missing or unparseable, the orchestrator must initialize from Phase 0 (not guess). If re-validation fails, the orchestrator must update state before proceeding — never resume from stale state. diff --git a/.github/skills/rewrite-assessment/SKILL.md b/.github/skills/rewrite-assessment/SKILL.md index 3421977..56174f7 100644 --- a/.github/skills/rewrite-assessment/SKILL.md +++ b/.github/skills/rewrite-assessment/SKILL.md @@ -173,3 +173,16 @@ This assessment **must** produce at least one ADR: - If the user hasn't specified a target stack, ask before proceeding. Assessment without a target is meaningless. - Effort estimates should include ramp-up time, testing, migration, and parallel-run periods — not just coding. - Do not minimize the cost of knowledge embedded in existing code. Implicit behavior is expensive to rediscover. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking rewrite-assessment as complete: + +- [ ] `specs/assessment/rewrite.md` exists with: feasibility rating, effort comparison (rewrite vs modernize), risk analysis, and target stack evaluation +- [ ] A rewrite-vs-modernize recommendation is stated with supporting evidence +- [ ] Strangler fig (partial rewrite) option is explicitly evaluated +- [ ] Data migration complexity is assessed +- [ ] At least one ADR exists in `specs/adrs/` documenting the rewrite/modernize decision +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to planning. diff --git a/.github/skills/rewrite-planner/SKILL.md b/.github/skills/rewrite-planner/SKILL.md index e76498d..976388a 100644 --- a/.github/skills/rewrite-planner/SKILL.md +++ b/.github/skills/rewrite-planner/SKILL.md @@ -232,3 +232,17 @@ Before finalizing, verify: After approval at the human gate, each increment proceeds through Phase 2: test generation → contract generation → implementation → build & deploy. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking rewrite-planner as complete: + +- [ ] `specs/increment-plan.md` is updated with all rewrite increments (unique IDs, scope, dependencies, effort) +- [ ] Each increment rewrites exactly one component (no multi-component increments) +- [ ] Strangler fig routing (feature flags) is planned for each increment +- [ ] Behavioral equivalence tests are specified before any enhancement increments +- [ ] Every increment references its justifying ADR +- [ ] Gherkin deltas document both old behavior (to preserve) and new behavior (to verify) +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to Phase 2 delivery. diff --git a/.github/skills/security-assessment/SKILL.md b/.github/skills/security-assessment/SKILL.md index 6279309..71a2715 100644 --- a/.github/skills/security-assessment/SKILL.md +++ b/.github/skills/security-assessment/SKILL.md @@ -190,3 +190,17 @@ Generate ADRs via the `adr` skill for major security architecture decisions: - Focus remediation guidance on practical steps the team can take, not theoretical best practices. - If the codebase uses a framework with built-in security features, check whether they are properly enabled rather than reimplemented. - Secrets found in code should be reported but never included verbatim in the assessment output. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking security-assessment as complete: + +- [ ] `specs/assessment/security.md` exists with: executive summary, findings by OWASP category, severity ratings, and remediation guidance +- [ ] Every finding has a severity level (critical / high / medium / low) and a clear remediation path +- [ ] Dependency CVE scan results are included (even if no CVEs found — state "0 known CVEs") +- [ ] Authentication and authorization patterns are reviewed and documented +- [ ] Secrets-in-code scan completed (no secrets included verbatim in output) +- [ ] At least one ADR exists in `specs/adrs/` for significant security architecture decisions +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to planning. diff --git a/.github/skills/security-planner/SKILL.md b/.github/skills/security-planner/SKILL.md index 489391b..db3adb3 100644 --- a/.github/skills/security-planner/SKILL.md +++ b/.github/skills/security-planner/SKILL.md @@ -239,3 +239,16 @@ proceeds through the standard Phase 2 pipeline: 2. **Contract generation** — update contracts if API behavior changes 3. **Implementation** — apply the minimal fix 4. **Build & deploy** — verify the fix in CI, re-run security scanners + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking security-planner as complete: + +- [ ] `specs/increment-plan.md` is updated with all security fix increments (unique IDs, scope, severity, effort) +- [ ] Increments are ordered by severity: critical → high → medium → low (no exceptions) +- [ ] Every fix increment has a corresponding reproduction test specified +- [ ] Increments are consistent with security-related ADRs; conflicts are flagged +- [ ] Dependency CVE fixes are separate increments from code-level vulnerability fixes +- [ ] State JSON and audit log are updated + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to Phase 2 delivery. diff --git a/.github/skills/state-management/SKILL.md b/.github/skills/state-management/SKILL.md index d3864ee..6ffc8c6 100644 --- a/.github/skills/state-management/SKILL.md +++ b/.github/skills/state-management/SKILL.md @@ -88,3 +88,16 @@ When `track` is `"hybrid"`, each increment in Phase 2 inherits its track from `f - Verification step: check deployment status 4. If results match state → continue from where you left off 5. If results differ → update state to reflect reality, then continue + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following after every state write: + +- [ ] `.spec2cloud/state.json` is valid JSON (parseable without errors) +- [ ] `currentPhase` reflects the actual phase the orchestrator is in +- [ ] `currentIncrement` and increment step match the work just completed +- [ ] `lastUpdated` timestamp is current +- [ ] No fields are null/undefined that downstream skills depend on (e.g., `adrs.nextNumber`, `incrementPlan`) +- [ ] State file is committed alongside the artifacts it describes (not orphaned) + +**BLOCKING**: If state.json is invalid or inconsistent with actual artifacts, the `resume` skill will reconstruct the wrong position. Fix state before proceeding. diff --git a/.github/skills/tech-stack-resolution/SKILL.md b/.github/skills/tech-stack-resolution/SKILL.md index 7fa42f5..f023eae 100644 --- a/.github/skills/tech-stack-resolution/SKILL.md +++ b/.github/skills/tech-stack-resolution/SKILL.md @@ -145,3 +145,17 @@ Before presenting to the human for approval: - [ ] No technology in the increment plan without being in tech-stack.md - [ ] Skills exist for non-trivial technologies - [ ] Instructions in copilot-instructions.md for project-wide conventions + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking tech-stack-resolution as complete: + +- [ ] `specs/tech-stack.md` contains an "Infrastructure Resources" section with a table of all Azure resources +- [ ] `specs/contracts/infra/resources.yaml` exists and lists all Azure resources with types, SKUs, and increment mappings +- [ ] Every technology decision that requires an Azure resource has that resource documented in both tech-stack.md and resources.yaml +- [ ] Per-increment infrastructure map shows which resources and env vars each increment needs +- [ ] At least one ADR exists in `specs/adrs/` for significant technology choices (e.g., cloud provider, AI service, database) +- [ ] `infra/main.bicep` is checked for gaps against the infrastructure contract — every resource in resources.yaml has a corresponding Bicep definition +- [ ] Authentication model is documented (managed identity vs API keys vs connection strings) + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to increment delivery. diff --git a/.github/skills/test-discovery/SKILL.md b/.github/skills/test-discovery/SKILL.md index 0b5e405..48acc91 100644 --- a/.github/skills/test-discovery/SKILL.md +++ b/.github/skills/test-discovery/SKILL.md @@ -296,3 +296,15 @@ _Extracted on [date]. Catalogs all tests that exist in the project._ 8. **Uncertain mappings are okay.** If you cannot determine what a test file tests, say "mapping uncertain" rather than guessing. Partial information is better than fabricated information. + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking test-discovery as complete: + +- [ ] `specs/docs/testing/coverage.md` exists with: test framework(s), total test count, pass/fail/skip breakdown +- [ ] Every test file in the project is cataloged with its framework and approximate test count +- [ ] Test-to-source mapping is documented where determinable (which tests cover which source files) +- [ ] Skipped/pending/todo tests are counted separately and listed +- [ ] Coverage reports (if they exist as files) are referenced with their location and date + +**BLOCKING**: If any item is unchecked, the skill has NOT completed successfully. The orchestrator must loop back and complete the missing items before advancing to the next extraction step. diff --git a/.github/skills/test-generation/SKILL.md b/.github/skills/test-generation/SKILL.md index 9f45ab1..85e3c52 100644 --- a/.github/skills/test-generation/SKILL.md +++ b/.github/skills/test-generation/SKILL.md @@ -14,7 +14,7 @@ description: >- You are the Test Generation Agent. You read approved Gherkin scenarios from `specs/features/*.feature` and generate BDD test code: Cucumber step definitions and Vitest unit/integration tests. Your output is a **red baseline** — all tests exist, all tests compile/parse, and all tests FAIL because no application code exists yet. This is the test-driven contract that the Implementation Agent must satisfy. -**You do NOT generate Playwright e2e tests** — those are already created in Phase 3 by the E2E Generation Agent. You generate Cucumber step definitions (which may use the Page Object Models from Phase 3) and Vitest backend tests. +**You do NOT generate Playwright e2e tests** — those are already created in Phase 2 Step 1a by the `e2e-generation` skill. You generate Cucumber step definitions (which may use the Page Object Models from Step 1a) and Vitest backend tests. You do not write application code. You do not make tests pass. You DO write fully implemented test code — real HTTP calls, real Playwright interactions in Cucumber steps, real assertions — that will fail because the application endpoints, pages, and services don't exist yet. A step definition with `throw new Error('Not implemented')` or an empty body is NOT a deliverable. @@ -44,7 +44,7 @@ Before you begin, read and understand: 1. **FRDs** (`specs/frd-*.md`) — for domain context and acceptance criteria 2. **Gherkin scenarios** (`specs/features/*.feature`) — your primary input; every step becomes a test assertion -3. **Page Object Models** (`e2e/pages/*.page.ts`) — generated in Phase 3; Cucumber step definitions that involve UI interactions should use these POMs +3. **Page Object Models** (`e2e/pages/*.page.ts`) — generated in Phase 2 Step 1a by `e2e-generation`; Cucumber step definitions that involve UI interactions should use these POMs 4. **Existing project structure** — respect conventions already in place 5. **`.spec2cloud/state.json`** — confirm you are in Phase 2 (increment delivery), Step 1c (BDD Test Scaffolding) 6. **Increment plan** (`specs/increment-plan.md`) — identify which features are in scope for the current increment @@ -138,7 +138,7 @@ describe('User Authentication', () => { > - **Unit tests** (`src/api/tests/unit/`): Test individual service functions, validators, and handlers in isolation using `vi.mock()` for dependencies > - **Integration tests** (`src/api/tests/integration/`): Test HTTP endpoints using Supertest against the full Express app -> **Playwright e2e specs and Page Object Models** are generated in Phase 3 by the E2E Generation Agent. Do NOT create new `e2e/*.spec.ts` or `e2e/pages/*.page.ts` files. If Cucumber step definitions need UI interactions, import the existing POMs from `e2e/pages/`. +> **Playwright e2e specs and Page Object Models** are generated in Phase 2 Step 1a by the `e2e-generation` skill. Do NOT create new `e2e/*.spec.ts` or `e2e/pages/*.page.ts` files. If Cucumber step definitions need UI interactions, import the existing POMs from `e2e/pages/`. --- @@ -157,10 +157,10 @@ project-root/ │ └── support/ │ ├── world.ts # Cucumber World (shared state: page, request context) — DO NOT MODIFY │ └── hooks.ts # Before/After hooks (Aspire startup, screenshots) — DO NOT MODIFY -├── e2e/ # ALREADY GENERATED in Phase 3 — do not create/modify +├── e2e/ # ALREADY GENERATED in Phase 2 Step 1a — do not create/modify │ ├── playwright.config.ts -│ ├── *.spec.ts # E2E flow specs (from Phase 3) -│ └── pages/ # Page Object Models (from Phase 3) — import in Cucumber steps +│ ├── *.spec.ts # E2E flow specs (from Step 1a) +│ └── pages/ # Page Object Models (from Step 1a) — import in Cucumber steps ├── src/api/tests/ │ ├── unit/ │ │ ├── user-auth.test.ts @@ -193,7 +193,7 @@ Read the `.feature` file. Identify: ### Step 2: Classify Each Scenario -Determine which test layers apply (Playwright e2e is already generated in Phase 3): +Determine which test layers apply (Playwright e2e is already generated in Phase 2 Step 1a): | Tag / Content | Cucumber Steps | Vitest Tests | |---|---|---| @@ -210,7 +210,7 @@ For each feature, create all applicable test files following the patterns in the - Every Gherkin step has a corresponding step definition - Every API-related scenario has Vitest unit tests for the underlying services -- Cucumber step definitions that involve UI use the POMs from Phase 3 (`e2e/pages/`) +- Cucumber step definitions that involve UI use the POMs from Phase 2 Step 1a (`e2e/pages/`) - Shared steps are extracted to `common.steps.ts` ### Step 4: Generate Project Configuration @@ -322,11 +322,11 @@ cd src/api && npm test ``` All tests should **compile** but **fail** at runtime because no application logic exists yet. -### 3. Playwright E2E (from Phase 3) +### 3. Playwright E2E (from Phase 2 Step 1a) ```bash npx playwright test --list ``` -Verify all e2e tests from Phase 3 are still listed. Do NOT modify or re-generate them. +Verify all e2e tests from Phase 2 Step 1a are still listed. Do NOT modify or re-generate them. ### 4. Validation Rule **If any test passes, something is wrong.** A passing test means either: diff --git a/.github/skills/test-runner/SKILL.md b/.github/skills/test-runner/SKILL.md index 614f92f..8afbe9f 100644 --- a/.github/skills/test-runner/SKILL.md +++ b/.github/skills/test-runner/SKILL.md @@ -65,3 +65,15 @@ When tests fail against the Aspire environment: - Aspire resources unhealthy → restart via `aspire start` (auto-stops previous) - Tests exceed 5 minutes → check for hung processes - Always capture both stdout and stderr + +## Mandatory Completion Checklist + +The orchestrator MUST verify ALL of the following before marking test-runner as complete: + +- [ ] All test suites were executed (unit, integration, e2e, Cucumber) — none skipped +- [ ] Results include: total tests, passed, failed, skipped counts per suite +- [ ] Any failures include file path, test name, and error message +- [ ] Aspire environment was healthy during test execution (resources Running + Healthy) +- [ ] If any tests failed, the failure is reported as a blocking issue — not silently ignored + +**BLOCKING**: If the test runner itself fails (infrastructure failure, timeout, partial execution), this must be reported as "run incomplete" — not as "all tests passed". diff --git a/.github/skills/ui-ux-design/SKILL.md b/.github/skills/ui-ux-design/SKILL.md index 4e95415..90692c0 100644 --- a/.github/skills/ui-ux-design/SKILL.md +++ b/.github/skills/ui-ux-design/SKILL.md @@ -19,7 +19,7 @@ You are the **UI/UX design agent** for the spec2cloud pipeline. Your job is to t - Approved PRD (`specs/prd.md`) - Approved FRDs (`specs/frd-*.md`) -- Project stack info from `AGENTS.md` §11 +- Project stack info from `AGENTS.md` §7 (Stack Reference) ## Process diff --git a/.spec2cloud/state.json b/.spec2cloud/state.json index d32d2ec..24fdad6 100644 --- a/.spec2cloud/state.json +++ b/.spec2cloud/state.json @@ -21,8 +21,15 @@ "activePaths": [], "assessment": {}, "adrs": { - "nextNumber": 1, - "records": [] + "nextNumber": 2, + "records": [ + { + "id": "adr-001", + "title": "Use Azure OpenAI Service instead of Direct OpenAI API", + "status": "accepted", + "path": "specs/adrs/adr-001-azure-openai-over-direct-openai.md" + } + ] }, "incrementPlan": [], "currentIncrement": null, @@ -39,5 +46,11 @@ "plan-approved": false }, "testsStatus": {}, - "lastUpdated": "2026-03-16T00:00:00Z" + "lastUpdated": "2026-03-30T16:55:00Z", + "infrastructureContract": { + "status": "complete", + "lastUpdated": "2026-03-30T16:55:00Z", + "path": "specs/contracts/infra/resources.yaml", + "resources": ["container-apps-api", "container-apps-web", "azure-ai-services", "container-registry", "log-analytics", "app-insights", "container-apps-env", "managed-identity-api", "managed-identity-web", "ai-foundry-project"] + } } diff --git a/AGENTS.md b/AGENTS.md index 8fe4ba8..73dfade 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,26 @@ You are the **spec2cloud orchestrator**. You drive a project from human-language 11. If not → loop back to 1 ``` +**Loop step enforcement:** +- Steps 1, 6, 7, 9 are **mandatory** — never skip state reads, execution, verification, or state updates. +- Steps 3-5 (skill check, skill search, research) are **mandatory-or-logged**: if skipped, the audit log must record WHY (e.g., `action=skill-check-skipped reason="task is a protocol step, no skill applies"`). +- Step 8 (skill creation) is **conditional** — only triggers when a genuinely reusable pattern emerges. No audit entry needed when skipped. +- Step 7 (verify) must produce a concrete result: tests pass/fail, file exists/missing, build succeeds/fails. "Looks good" is not verification. + You are monolithic: one process, one task per loop iteration. You invoke skills from `.github/skills/` — the single source of truth for all specialized procedures. +### Prerequisite Verification Rule + +Before invoking any skill, the orchestrator MUST verify that the skill's input artifacts exist. Every skill consumes artifacts produced by earlier phases — if those artifacts are missing, the skill will produce incomplete or incorrect output. + +**Preflight check pattern:** +1. Read the skill's SKILL.md "Inputs" or "Prerequisites" section +2. Verify each listed file/directory exists (e.g., `specs/features/*.feature`, `specs/ui/screen-map.md`) +3. If any prerequisite is missing → **STOP** and report which artifact is missing and which upstream skill should have produced it +4. Do NOT proceed with partial inputs — partial inputs cause cascading gaps + +This applies to every skill invocation, including research and protocol skills. + --- ## 2. Skills Catalog @@ -69,8 +87,11 @@ All specialized logic lives in `.github/skills/` following the [agentskills.io]( | `research-best-practices` | Query MCP tools for current best practices | | `skill-creator` | Create new agentskills.io-compliant skills | | `skill-discovery` | Search skills.sh for community skills | +| `find-skills` | Help users discover and install skills for specific tasks | | `adr` | Generate and manage Architecture Decision Records | | `bug-fix` | Lightweight bug fix with FRD traceability | +| `aspire` | Orchestrate Aspire distributed apps (start, stop, describe, logs) | +| `playwright-cli` | Automate browser interactions for testing, screenshots, data extraction | ### Brownfield Common Trunk Skills (Phase B0-B2 — always run) @@ -139,7 +160,18 @@ Phase 2: Increment Delivery (repeats per increment) **Goal:** Repository ready — scaffolding, config, conventions in place. **Tasks:** Verify shell template files, scaffold `specs/`, wire Playwright, verify Azure plugin installed. -**Exit:** All required files exist. **Human gate:** Yes. + +**Required files (all must exist before Phase 0 exits):** +- [ ] `specs/` directory exists +- [ ] `.spec2cloud/state.json` exists and is valid JSON +- [ ] `.spec2cloud/audit.log` exists +- [ ] `.github/copilot-instructions.md` exists +- [ ] `AGENTS.md` exists +- [ ] Shell template files are present (apphost.cs, package.json, etc.) +- [ ] Playwright is wired (`e2e/` directory structure or config present) +- [ ] Azure plugin installed (`azd version` succeeds) + +**Exit:** All required files verified. **Human gate:** Yes. **Commit:** `[phase-0] Shell setup complete` ### Phase 1: Product Discovery @@ -149,7 +181,7 @@ Review PRD/FRDs through product + technical lenses (max 5 passes). Break PRD int **Exit:** Human approves all FRDs. **Human gate:** Yes. #### 1b: UI/UX Design → `ui-ux-design` skill -Follow the skill’s 8-step process (Screen Inventory → Design System → HTML Prototypes → Component Inventory → Serve → Walkthrough → Review Loop → Cleanup). The skill produces **6 mandatory artifacts** — all must exist before the phase can exit: +Follow the skill's 8-step process (Screen Inventory → Design System → HTML Prototypes → Component Inventory → Serve → Walkthrough → Review Loop → Cleanup). The skill produces **6 mandatory artifacts** — all must exist before the phase can exit: | Artifact | Path | Consumed By | |----------|------|-------------| @@ -175,6 +207,22 @@ Resolve every technology, library, service. Research via MCP tools. Search skill **Exit:** Human approves. **Human gate:** Yes. **Commit:** `[phase-1] Product discovery complete — N FRDs, N screens, N increments, tech stack resolved` +#### Phase 1 Exit Checklist + +The orchestrator MUST verify ALL of the following before transitioning from Phase 1 to Phase 2: + +- [ ] `specs/prd.md` exists (or FRDs were provided directly) +- [ ] At least one `specs/frd-*.md` exists and is human-approved +- [ ] All 6 UI/UX artifacts exist in `specs/ui/` (screen-map, design-system, prototypes, component-inventory, flow-walkthrough, walkthrough.html) +- [ ] `specs/increment-plan.md` exists with at least one increment defined +- [ ] `specs/tech-stack.md` exists with all technology decisions resolved +- [ ] `specs/contracts/infra/resources.yaml` exists (if Azure resources are needed) +- [ ] At least one ADR exists in `specs/adrs/` for significant technology choices +- [ ] `.spec2cloud/state.json` reflects Phase 1 completion +- [ ] All human gates for Phase 1 are recorded as approved in state + +**BLOCKING**: If any item is unchecked, Phase 2 cannot begin. The orchestrator must complete the missing items. + ### Phase 2: Increment Delivery (per increment) ``` @@ -183,7 +231,16 @@ Resolve every technology, library, service. Research via MCP tools. Search skill main green + deployed ``` -> **⚠ MANDATORY:** Every step must execute in order. No step may be skipped, reordered, or compressed. Tests from Step 1 are the proof that specs are met — they are the contract. +> **⚠ MANDATORY:** Every step must execute in order. No step may be skipped, reordered, or compressed. Tests from Step 1 are the proof that specs are met — they are the contract. See §9 Test Discipline Gospel. + +**Step Transition Gates:** Before advancing from one step to the next, verify: + +| Transition | Prerequisite Check | +|------------|-------------------| +| Step 1 → Step 2 | Test files exist, compile, and FAIL (red baseline). No `test.skip()` in codebase. Existing tests still pass. Human approved Gherkin and test code. | +| Step 2 → Step 3 | Contract files exist in `specs/contracts/api/`. Shared types compile. Infra contract updated if needed. | +| Step 3 → Step 4 | ALL tests pass (new + existing). Zero failures. PR review approved by human. | +| Step 4 → Next Increment | Deploy succeeded. Smoke tests pass. Deployment URLs recorded in state. Human verified deployment. | #### Step 1: Test Scaffolding - **1a** `e2e-generation` — Playwright specs + POMs from flow walkthrough @@ -303,15 +360,17 @@ The orchestrator presents a testability checklist. The human assesses and decide **Decision outcomes:** -| Outcome | Track | State value | Description | -|---------|-------|-------------|-------------| -| All/most checked | **Track A** | `testability: "full"` | Full green baseline with executable tests | -| Some checked | **Track Hybrid** | `testability: "partial"` | Track A for testable features, Track B for the rest | -| Few/none checked | **Track B** | `testability: "none"` | Behavioral documentation only | +| Outcome | Track | State value | Threshold | Description | +|---------|-------|-------------|-----------|-------------| +| 5-6 checked | **Track A** | `testability: "full"` | App builds, starts, and can be exercised via HTTP + browser | Full green baseline with executable tests | +| 3-4 checked | **Track Hybrid** | `testability: "partial"` | Some features exercisable, others blocked by deps/env | Track A for testable features, Track B for the rest | +| 0-2 checked | **Track B** | `testability: "none"` | Cannot build/start or most deps unreachable | Behavioral documentation only | For hybrid mode, the human also identifies which features are testable: `featureTracks: { "auth": "A", "search": "A", "reporting": "B" }` stored in state.json. +> **Mandatory:** The human MUST document the rationale for the track decision in an ADR (`specs/adrs/adr-NNN-testability-gate.md`). The ADR must list which checklist items passed/failed and why the chosen track is appropriate. This decision is recorded in state.json and cannot be changed without a new ADR. + **Human gate:** Yes — this is a critical decision point. ### Track A: Green Baseline (Testable Apps) @@ -462,12 +521,23 @@ After planning, all paths (modernize, rewrite, extend, security, etc.) produce i Architecture Decision Records are first-class artifacts in both greenfield and brownfield workflows. ### When ADRs Are Generated -- Greenfield Phase 1d (Tech Stack): Every significant technology choice -- Brownfield Testability Gate: Track selection decision (ADR documenting testability assessment) -- Brownfield Phase A (Assessment): Every path decision and significant finding -- Phase 2 Step 2 (Contracts): Significant API/contract design decisions -- Phase 2 Step 3 (Implementation): Deviations from established patterns -- Any human gate that results in a direction change + +An ADR is **mandatory** for any of the following triggering events: + +| Trigger | Phase | Example | +|---------|-------|---------| +| New Azure resource added | 1d, 2.2 | Adding Azure AI Services, switching database provider | +| New framework or runtime adopted | 1d | Choosing LangGraph, selecting Next.js over Remix | +| Authentication/authorization model decision | 1d, A | Managed identity vs API keys, OAuth vs JWT | +| Cloud provider or deployment target chosen | 1d | Azure Container Apps vs AKS | +| Testability gate track selection | B2 | Track A vs B vs Hybrid decision with rationale | +| Assessment path decision | A | Modernize vs rewrite decision | +| API design pattern chosen | 2.2 | REST vs GraphQL, pagination strategy | +| Deviation from established pattern | 2.3 | Using a different approach than what tech-stack.md specifies | +| Human gate direction change | Any | Human rejects and redirects to a different approach | +| Dependency substitution | A, P | Replacing an unmaintained library | + +> **Rule:** If in doubt, write the ADR. An unnecessary ADR costs minutes; an undocumented decision costs hours of confusion later. ### ADR Lifecycle Status: proposed → accepted (or rejected) → deprecated/superseded @@ -600,6 +670,8 @@ infra/ # Azure Bicep templates | `azd down` | Tear down all resources | +--- + --- ## 8. Research Protocol @@ -607,3 +679,99 @@ infra/ # Azure Bicep templates Before writing implementation code, invoke the `research-best-practices` skill. Consult `specs/tech-stack.md` first — most technology decisions are pre-resolved in Phase 1d. For details, see the `research-best-practices` skill in `.github/skills/`. + +--- + +## 9. Test Discipline Gospel + +Tests are the **proof** that specifications have been implemented. They are not optional, not skippable, and not negotiable. Every test in the pipeline exists because a spec demands it. Skipping a test is equivalent to silently removing a requirement. + +### Absolute Rules + +1. **Tests are proof of spec completion.** A feature is not done until every test generated from its Gherkin scenarios passes. No exceptions. +2. **Never skip a test.** Do not use `test.skip()`, `xit()`, `@skip`, `pending`, or any mechanism that bypasses test execution. If a test cannot run, fix the infrastructure — do not skip the test. +3. **Never delete a test.** Tests are the contract. Deleting a test removes proof that a spec was met. If a spec changes, update the test to match the new spec — do not delete the old one without replacement. +4. **Never modify a test without human approval.** Tests are human-approved contracts. Any change to a test requires explicit human consent because it changes what "done" means. +5. **The phase pipeline is sacred.** The sequence Tests → Contracts → Implementation → Verify is not a suggestion. Every increment must pass through every step. The orchestrator must not skip, reorder, or compress these steps. +6. **All tests must pass before advancing.** No phase transition, no deployment, no commit to main happens while any test is failing. A red test is a blocking issue. +7. **Regression is mandatory.** After every change (implementation, bug fix, refactor), the FULL test suite runs. Not just the new tests — all tests. + +### Mid-Flow Bug-Spot Protocol + +When a user spots something wrong at **any point** during the spec2cloud flow — during a review, during deployment verification, during normal usage — the orchestrator activates the **bug-spot protocol**: + +``` +1. PAUSE current work +2. ACKNOWLEDGE the problem — summarize what the user reported +3. IDENTIFY the relevant FRD and acceptance criterion +4. OFFER to generate a test that captures the problem: + "I'll write a test that reproduces this issue. Would you like to review it?" +5. GENERATE the failing test (following bug-fix skill conventions) +6. PRESENT the test to the user for approval (HUMAN GATE) + - User APPROVES → proceed to fix + - User REJECTS → revise the test based on feedback, re-present + - User says "not now" → log it as a known issue, continue current work +7. VERIFY the test fails (proves the bug exists) +8. FIX the code minimally +9. VERIFY the test passes AND full regression passes +10. RESUME the interrupted work from where it was paused +``` + +This protocol applies regardless of which phase is active. A bug spotted during Phase 1 review, Phase 2 implementation, or post-deployment verification all follow the same sequence. + +### Test Approval Gates + +Tests require human approval at these points: + +| When | What | Gate Type | Human Must Verify | +|------|------|-----------|-------------------| +| Step 1b (Gherkin) | Generated Gherkin scenarios | Human reviews scenarios match FRD intent | Every acceptance criterion has a scenario; steps use domain language; error/edge cases covered; tags applied correctly | +| Step 1c (Tests) | Generated test code | Human reviews test logic matches Gherkin | Every Gherkin step has a step definition; assertions are meaningful (not empty); test structure follows conventions | +| Bug-spot protocol | Bug-reproducing test | Human approves the test captures the real problem | Test fails for the right reason; test will pass when the bug is fixed; test doesn't over-specify | +| Brownfield Track A | Green baseline tests | Human verifies tests describe actual behavior | Tests pass against running app; scenarios describe what IS, not what SHOULD BE; coverage matches FRD feature areas | +| Any test modification | Changed test code | Human approves the change to the contract | Change is justified by a spec change; no weakening of assertions; regression coverage maintained | + +### All Human Gates — Review Rubrics + +| Gate | Phase | What the Human Must Check | +|------|-------|--------------------------| +| Phase 0 exit | 0 | All required files exist (see checklist above); project builds | +| FRD approval | 1a | Every PRD requirement traced to an FRD; acceptance criteria are testable; no ambiguity | +| UI/UX approval | 1b | All 6 artifacts present; screens match FRDs; `data-testid` selectors on interactive elements; navigation flows complete | +| Increment plan approval | 1c | Walking skeleton is first; dependency order is correct; no oversized increments; all FRDs covered | +| Tech stack approval | 1d | All technologies resolved with versions; infra contract exists; ADRs for significant choices | +| PR review | 2.3 | Code makes tests pass without hacks; follows conventions; no hardcoded secrets/URLs; error handling present | +| Deployment verification | 2.4 | App is accessible at deployed URL; core user flows work; no console errors; smoke tests pass | +| Brownfield extraction review | B1 | Extraction is factual; no opinions/recommendations in outputs; all source files covered | +| Testability gate | B2 | Checklist items are honestly assessed; track choice matches reality; ADR documents rationale | + +### What "Skipping" Looks Like (All Prohibited) + +- ❌ `test.skip("should validate email format", ...)` — skipping a test +- ❌ Commenting out a test body or assertion +- ❌ Moving to Step 3 (Implementation) before Step 1 (Tests) completes +- ❌ Deploying with failing tests ("we'll fix it later") +- ❌ Marking a test as `@pending` or `@wip` to avoid failures +- ❌ Running only "relevant" tests instead of the full suite at phase gates +- ❌ Removing a Gherkin scenario without updating the FRD first +- ❌ Proceeding after `npm test` shows failures ("only 2 failing, the rest pass") + +### Automated Skip Detection + +The orchestrator MUST run the following scan at every phase gate (Step 1→2, Step 3→4, before deployment): + +```bash +# Detect prohibited test-skipping patterns in test files +grep -rn --include="*.ts" --include="*.js" --include="*.feature" \ + -E "(test\.skip|it\.skip|describe\.skip|xit\(|xdescribe\(|@skip|@pending|@wip|\.only\()" \ + tests/ e2e/ specs/features/ src/*/tests/ 2>/dev/null +``` + +If this scan returns ANY matches, the orchestrator MUST: +1. Report every match (file, line, pattern) +2. **BLOCK** advancement until all matches are removed +3. Log the violation in the audit log: `action=skip-detected result=blocked` + +### The Only Exception: Track B (Non-Testable) + +Track B brownfield features that cannot be tested (per the testability gate decision) use manual verification checklists instead of automated tests. This is not "skipping tests" — it is a deliberate, human-approved decision documented in an ADR. Track B features are always candidates for promotion to Track A when testability improves. diff --git a/docs/README.md b/docs/README.md index fd77a0d..11a5a78 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ ## Deep Dives -- [Skills Catalog](skills.md) — All 43 skills with modes and interactions +- [Skills Catalog](skills.md) — All 46 skills with modes and interactions - [State & Human Gates](state-and-gates.md) — Resumability, auditability, approval checkpoints - [Architecture](architecture.md) — The Ralph Loop internals and directory structure - [Shell Templates](shells.md) — Pre-configured project scaffolds diff --git a/docs/architecture.md b/docs/architecture.md index d5cbcf1..3744600 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -47,7 +47,7 @@ State is committed to git after every action. This means: ``` .github/ -├── skills/ — 43 agentskills.io skills (reusable workflow components) +├── skills/ — 46 agentskills.io skills (reusable workflow components) └── copilot-instructions.md — Stack-specific coding conventions .spec2cloud/ — State persistence diff --git a/docs/brownfield.md b/docs/brownfield.md index f9d1d41..8dff16d 100644 --- a/docs/brownfield.md +++ b/docs/brownfield.md @@ -67,11 +67,11 @@ This is the most important brownfield decision. After specs are generated, you a | Outcome | Track | What happens next | |---------|-------|-------------------| -| All/most checked | **Track A** (Testable) | Green baseline — Gherkin + tests that pass on current app | -| Few/none checked | **Track B** (Doc-Only) | Behavioral documentation + manual verification checklists | -| Mixed | **Hybrid** | Track A for testable features, Track B for the rest | +| 5-6 checked | **Track A** (Testable) | Green baseline — Gherkin + tests that pass on current app | +| 0-2 checked | **Track B** (Doc-Only) | Behavioral documentation + manual verification checklists | +| 3-4 checked | **Hybrid** | Track A for testable features, Track B for the rest | -**Human Gate:** This is a critical decision point — it shapes the entire rest of the workflow. +**Human Gate:** This is a critical decision point — it shapes the entire rest of the workflow. The decision must be documented in an ADR (`specs/adrs/`) with rationale. --- diff --git a/docs/concepts.md b/docs/concepts.md index b676e32..bce3c72 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -20,14 +20,14 @@ The loop maintains deterministic progress through `state.json`, committed to git Skills are reusable procedures that do the actual work. Each skill has a SKILL.md file with instructions, optional references, and scripts. The orchestrator discovers and invokes them automatically. -**43 skills in 7 categories:** +**46 skills in 7 categories:** | Category | What they do | Count | |----------|-------------|-------| | Phase | Drive discovery (spec refinement, UI/UX, tech stack) | 3 | | Delivery | Execute increments (tests, contracts, implementation, deploy) | 6 | | Protocol | Ensure consistency (state, commits, audit, gates, resume, errors) | 6 | -| Utility | Support tools (validator, runner, build check, diagnostics) | 8 | +| Utility | Support tools (validator, runner, build check, diagnostics, Aspire, Playwright) | 11 | | Extraction | Scan existing codebases (scanner, deps, arch, API, data, tests) | 6 | | Assessment | Evaluate paths (modernize, rewrite, cloud-native, security, perf) | 5 | | Planning | Generate increments (modernize, rewrite, cloud-native, extend, security) | 5+4 | diff --git a/docs/examples/greenfield-walkthrough.md b/docs/examples/greenfield-walkthrough.md index 4c4dc36..8db8c95 100644 --- a/docs/examples/greenfield-walkthrough.md +++ b/docs/examples/greenfield-walkthrough.md @@ -1,64 +1,186 @@ -# Walkthrough: Building a Task Management App (Greenfield) +# Walkthrough: Building a Task Board App (Greenfield) -A team wants to build a task management application with boards, lists, cards, and collaboration — deployed on Azure. +Build a kanban Task Board app — from a one-page PRD to a live Azure deployment — in about 2 hours. This walkthrough mirrors the [spec2cloud Microhack](https://github.com/EmeaAppGbb/spec2cloud-microhack-greenfield) and shows what each phase looks like in practice. + +> **The core idea:** The most valuable asset in a codebase is not the code — it's the specification. Code is an implementation detail. With AI agents, it's also increasingly throwable. Given a good enough spec, you can regenerate the implementation at any time. What you can't regenerate is a precise, verified description of what the software is supposed to do. + +## What You'll Build + +A minimal kanban board: create tasks, move them through *To Do → In Progress → Done*, delete when finished. No login, no database — just an in-memory store. Simple enough to build in one session, complex enough to exercise the full pipeline. ## Starting Point -A one-page PRD describing a task management app. -## Phase 0: Shell Setup -Clone `spec2cloud-shell-nextjs-typescript`. Get a scaffold with Next.js, Express, Playwright, Cucumber, Vitest, and Bicep pre-configured. +A one-page PRD you write yourself, describing the Task Board in your own words. 5 user stories is plenty — the agents will ask clarifying questions if anything is ambiguous. + +--- + +## Phase 0: Shell Setup (~15 min) + +Create a repo from a shell template and connect to Azure: + +```bash +gh repo create my-task-board --template EmeaAppGbb/spec2cloud-shell-nextjs-typescript +cd my-task-board +npm install && cd src/web && npm install && cd ../.. && cd src/api && npm install && cd ../.. +azd auth login && azd env new microhack && azd env set AZURE_LOCATION swedencentral +``` + +You get: Next.js + Express + Playwright + Cucumber + Vitest + Bicep — all pre-wired with Aspire orchestration and 45 agentskills.io skills. + +> **Alternative shells:** The same pipeline works with [Python (FastAPI)](https://github.com/EmeaAppGbb/shell-python), [Java (Spring Boot)](https://github.com/EmeaAppGbb/shell-java), or [.NET (ASP.NET Core)](https://github.com/EmeaAppGbb/shell-dotnet). The orchestrator and skills are stack-agnostic. + +## Step 1: Write Your PRD (~20 min) + +This is the only step where you write anything substantial. Open `specs/prd.md` and describe your Task Board — what it does, who uses it, what the key user stories are, and any constraints (in-memory, no auth). + +> **Why this matters:** Your PRD is the ground truth everything traces back to. The more deliberately you write it, the more meaningful the verification is at every downstream gate. Vague requirements lead to passing tests that don't actually prove anything. -## Phase 1a: Spec Refinement -The orchestrator reviews the PRD. After 3 passes: -- Missing edge cases identified (1000+ cards on a board?) -- Permission model clarified (edit vs. view) -- PRD split into 4 FRDs: Board Management, Card Operations, Team Collaboration, Notifications +Then kick off the orchestrator. Start Copilot CLI in autonomous mode and use fleet to begin: -🚦 **Human Gate:** Team approves refined specs. +```bash +copilot --yolo +``` -## Phase 1b: UI/UX Design -HTML prototypes generated: board view, card modal, team settings, notification panel. Served on localhost. +Then in the Copilot session: -🚦 **Human Gate:** Approved with mobile layout feedback. +``` +/fleet I want to start the spec2cloud flow, the prd is already created, lets start with the review process and continue +``` -## Phase 1c: Increment Planning -3 increments: -1. Walking skeleton — Board CRUD + single card (proves architecture) -2. Full cards — Drag-and-drop, due dates, labels, attachments -3. Collaboration — Team members, permissions, notifications +--- + +## Phase 1: Product Discovery (~30 min) + +### 1a — Spec Refinement +The orchestrator reviews your PRD through product and technical lenses, splits it into FRDs, and flags gaps. Expect files like `specs/frd-tasks.md` and `specs/frd-board.md`. + +> **💡 Best practice:** Don't just accept the agent's fixes wholesale. Ask it to address gaps **one by one** and really pay attention to each proposed change. It's good practice to run the review multiple times until you're confident the PRD captures exactly what you want. Once the PRD is solid, ask the agent to review each FRD with the same rigour — treat FRDs as first-class specs, not just generated artifacts. + +🚦 **Human Gate — spec verification:** Check that every user story from your PRD is represented in an FRD with clear acceptance criteria. If something is wrong here, fix it now — anything that slips through will be tested, implemented, and deployed as-is. + +### 1b — UI/UX Design +Interactive HTML wireframe prototypes are generated and served on localhost. Walk through the board layout, task creation, edit/delete flows. + +🚦 **Human Gate — prototype verification:** Does the UI match what you had in mind when you wrote the PRD? An adjustment here costs seconds. The same change post-implementation costs much more. -## Phase 1d: Tech Stack -Researched and documented as ADRs: Next.js 14, Express + Prisma, PostgreSQL on Azure, Container Apps. +### 1c — Increment Planning +FRDs are broken into ordered increments. A typical plan: + +| Increment | What ships | +|-----------|-----------| +| 1 | Walking skeleton — empty board, API health check, wired up and deployed | +| 2 | Task CRUD — create, read, delete tasks; board renders tasks in correct column | +| 3 | Status transitions — move tasks forward/back; edit title and description | + +🚦 **Human Gate:** Approve the plan or ask to reorder. + +### 1d — Tech Stack Resolution +Every library is inventoried, researched via live docs, and pinned. Output: `specs/tech-stack.md` with ADRs for key decisions. 🚦 **Human Gate:** Tech stack approved. -## Phase 2, Increment 1: Walking Skeleton +--- + +## Phase 2: Increment Delivery (~60–75 min) + +Each increment runs four steps automatically: -**Step 1 — Tests:** -- 8 Playwright e2e specs, 12 Gherkin scenarios, 24 Vitest stubs -- All fail (red baseline ✅) +### Step 1 — Test Scaffolding +Tests are derived **directly from the Gherkin scenarios** in your FRDs — before any implementation code is written: +- **Gherkin feature files** — plain-English scenarios from your acceptance criteria +- **Playwright e2e tests** — browser-level flows (create task → move → delete) +- **Cucumber step definitions** — wiring Gherkin to executable code +- **Vitest unit tests** — API endpoint coverage -🚦 **Human Gate:** Gherkin approved. +🚦 **Human Gate — test verification:** Read the Gherkin scenarios. Do they describe the behaviour you wrote in your PRD? This is the most important gate in the entire pipeline — tests are the spec in executable form. If they're wrong, the implementation will be wrong too, and it will pass every test while doing so. -**Step 2 — Contracts:** -- OpenAPI: 6 endpoints (boards + cards CRUD) -- TypeScript types: Board, Card, User DTOs -- Bicep: Container App + PostgreSQL +After approval, the **red baseline** is established: all new tests fail (proving they're real), existing tests still pass. -**Step 3 — Implementation:** -- API + Web slices in parallel -- Integration wires them together -- All 44 tests pass 🟢 +### Step 2 — Contracts +API contracts, shared TypeScript types, and infrastructure requirements are generated. No human gate — contracts flow directly from the tests. -🚦 **Human Gate:** PR approved. +### Step 3 — Implementation +Three slices run: + +| Slice | What gets built | +|-------|----------------| +| **API slice** | Express routes, in-memory store, input validation | +| **Web slice** | Next.js board page, task cards, create/edit form, status controls | +| **Integration** | API + Web running together, full regression green | + +> **💡 Try it live:** Aspire is already running in the background. Ask the agent for the Aspire dashboard URL and open your app in the browser. Click around, test the flows, and see if anything feels off. If you spot bugs or issues, ask the agent to fix them — it can use the Aspire and Playwright MCP tools to diagnose and resolve problems interactively. It's much cheaper to catch issues here than after deployment. + +🚦 **Human Gate — implementation verification:** Review the diff. Does this match what you approved in the Gherkin? Tests passing is necessary — but not sufficient. You're verifying the *right* implementation, not just a green one. + +### Step 4 — Deploy to Azure +```bash +azd provision # Container Apps, ACR, monitoring +azd deploy # Build containers, push, deploy +``` + +Smoke tests run against the live URL — the same tests derived from your spec. + +🚦 **Human Gate — deployment verification:** Open the live app and walk through your original PRD user stories. Not the tests — *your original words*. + +--- + +## Verify Your Live App (~10 min) + +```bash +azd env get-values | grep SERVICE_WEB_ENDPOINT_URL +``` + +Open the URL and manually verify: +1. Create three tasks — one for each column +2. Move a task from To Do → In Progress → Done +3. Edit a task title +4. Delete a task + +--- + +## What Just Happened? + +| Phase | What the agents did | What you verified | +|-------|-------------------|------------------| +| Spec Refinement | Turned your PRD into traceable FRDs | Every user story is correctly interpreted | +| UI/UX Design | Generated interactive wireframes | The UI matches your intent | +| Increment Planning | Ordered delivery, walking skeleton first | Scope and ordering make sense | +| Tech Stack | Pinned correct library versions via live docs | Stack is appropriate | +| Test Scaffolding | Derived full test suite from FRDs | Tests faithfully express your acceptance criteria | +| Contracts | Generated API specs and shared types | — | +| Implementation | Wrote backend + frontend to make tests green | Implementation matches the Gherkin | +| Deployment | Provisioned Azure infra, ran smoke tests | PRD user stories work end-to-end in production | + +You wrote zero production code. You wrote a spec — and that spec drove every test, every line of implementation, and every deployment check. + +--- + +## Tear Down + +```bash +azd down +``` + +--- + +## Going Further + +- **Change your PRD mid-hack:** Add a new user story to an FRD and run Phase 2 for a new increment. +- **Explore the skills:** Read `.github/skills/spec-refinement/SKILL.md` to see how the agent reasons through a spec. +- **Try a different shell:** Replace the TypeScript shell with Python, Java, or .NET — the pipeline doesn't care what stack you're on. +- **Add persistence:** Write a `specs/frd-persistence.md` and let agents wire up Cosmos DB or PostgreSQL. +- **Run the full lab:** The structured [Microhack Lab](https://github.com/EmeaAppGbb/spec2cloud-microhack-greenfield/tree/main/docs/microhack-lab) has challenges, solutions, and detailed facilitator guides. + +--- -**Step 4 — Deploy:** -- `azd up` provisions Azure -- Smoke tests pass -- Full e2e regression green on production URL +## Troubleshooting -## Result -Working task management app live on Azure after Increment 1. Two more increments follow the same cycle. +| Symptom | Fix | +|---------|-----| +| Agent stuck in a loop | Ask: *"read the resume skill and continue from current state"* | +| Tests failing after implementation | Ask: *"run the test runner skill and fix any failures"* | +| Smoke tests fail after deploy | Agent auto-rolls back — review failure and re-approve | +| Wireframes not loading | Check terminal for the HTTP server URL | --- diff --git a/docs/greenfield.md b/docs/greenfield.md index 0d37795..62cb87c 100644 --- a/docs/greenfield.md +++ b/docs/greenfield.md @@ -8,6 +8,8 @@ Build production-ready applications from a product specification, deployed to Az Pick a shell template that matches your tech stack. The shell provides project structure, test frameworks, Azure infrastructure templates, and CI/CD workflows. +Before Phase 0 exits, all scaffolding must be verified: `specs/` directory, `.spec2cloud/state.json`, `.spec2cloud/audit.log`, `AGENTS.md`, shell template files, Playwright wiring, and Azure CLI (`azd`) installed. + | Template | Language | Stack | |----------|----------|-------| | **Next.js** | TypeScript | React + Express + PostgreSQL | @@ -39,10 +41,21 @@ Every technology choice is researched, evaluated, and documented as an Architect **Human Gate:** Review tech stack choices and ADRs. +Before moving to Phase 2, the orchestrator verifies all Phase 1 artifacts exist: PRD/FRDs, all 6 UI/UX artifacts, increment plan, tech stack, and infrastructure contract. + ## Phase 2: Increment Delivery ![Increment Delivery Cycle](assets/increment-delivery.svg) +Each step transition has a prerequisite gate: + +| Transition | Prerequisite | +|------------|-------------| +| Step 1 → 2 | Tests exist, compile, and FAIL (red baseline). No `test.skip()`. Human approved Gherkin + tests. | +| Step 2 → 3 | Contracts exist. Shared types compile. Infra contract updated. | +| Step 3 → 4 | ALL tests pass. PR review approved. | +| Step 4 → Next | Deploy succeeded. Smoke tests pass. Human verified. | + ### Step 1: Tests First - **E2E specs** (Playwright) from UI flows diff --git a/docs/shells.md b/docs/shells.md index 3ea6b1b..6c94899 100644 --- a/docs/shells.md +++ b/docs/shells.md @@ -2,14 +2,37 @@ Shells are pre-configured project scaffolds that give you a running start. Each shell provides the project structure, test frameworks, Azure infrastructure, and CI/CD workflows for a specific tech stack. -## Available Shells +## How Shells Are Discovered -| Shell | Tech Stack | Repository | -|-------|-----------|-----------| -| **Next.js + TypeScript** | Next.js, Express, Playwright, Cucumber, Vitest | [spec2cloud-shell-nextjs-typescript](https://github.com/EmeaAppGbb/spec2cloud-shell-nextjs-typescript) | -| **.NET** | ASP.NET Core, Blazor, .NET testing | [shell-dotnet](https://github.com/EmeaAppGbb/shell-dotnet) | -| **Agentic .NET** | .NET + AI Agents (LangGraph) | [agentic-shell-dotnet](https://github.com/EmeaAppGbb/agentic-shell-dotnet) | -| **Agentic Python** | Python + AI Agents (LangGraph) | [agentic-shell-python](https://github.com/EmeaAppGbb/agentic-shell-python) | +Shells are discovered **dynamically** from GitHub repositories tagged with the topic `spec2cloud-shell` in the configured organization. No central registry to maintain — just add the topic to your repo and it becomes available. + +### Discovery order +1. **Topic-based discovery** — repos with topic `spec2cloud-shell` in the org (default: `EmeaAppGbb`) +2. **Static registry** — `shells.json` in the spec2cloud repo (fallback) +3. **Built-in list** — hardcoded in the CLI (offline fallback) + +### Creating a New Shell + +1. Create a GitHub repository in your org +2. Add the topic `spec2cloud-shell` to the repository +3. Optionally add a `shell.json` at the repo root for rich metadata: + +```json +{ + "id": "my-shell", + "name": "My Custom Shell", + "desc": "React, FastAPI, Playwright, Pytest" +} +``` + +If `shell.json` is missing, the CLI derives metadata from the repository name and description. + +### Using shells from a different org + +```bash +npx spec2cloud init --org my-org --list-shells # List shells from my-org +npx spec2cloud init --org my-org --shell my-shell # Use a shell from my-org +``` ## What's in a Shell @@ -25,22 +48,21 @@ Every shell provides: ## Skills Work With Any Stack -The 43 skills are stack-agnostic. Shells provide the stack-specific wiring (which test runner to use, which build commands, which Azure resources), but the skills themselves—spec refinement, gherkin generation, implementation strategy—work identically regardless of your technology choice. +The 46 skills are stack-agnostic. Shells provide the stack-specific wiring (which test runner to use, which build commands, which Azure resources), but the skills themselves—spec refinement, gherkin generation, implementation strategy—work identically regardless of your technology choice. ## Starting from a Shell -1. Clone the shell repository -2. Run the installer: `./scripts/install.sh --full` -3. Open in VS Code with the dev container -4. Write your PRD in `specs/prd.md` -5. Start with `/prd` in Copilot Chat +1. Run the installer with a shell: `npx spec2cloud init --shell ` +2. Open in VS Code with the dev container +3. Write your PRD in `specs/prd.md` +4. Start with `/prd` in Copilot Chat ## Adding to an Existing Project If you already have a project, use merge mode: ```bash -./scripts/install.sh --merge +npx spec2cloud init --minimal ``` This adds spec2cloud's skills, agents, and state management without overwriting your existing files. The installer detects your stack and configures accordingly. diff --git a/docs/skills.md b/docs/skills.md index 5643e37..0deafb8 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -1,6 +1,6 @@ # Skills Catalog -spec2cloud uses 43 specialized skills following the [agentskills.io](https://agentskills.io) standard. Each skill is a reusable procedure stored in `.github/skills/` with a SKILL.md file and optional references, scripts, and assets. +spec2cloud uses 46 specialized skills following the [agentskills.io](https://agentskills.io) standard. Each skill is a reusable procedure stored in `.github/skills/` with a SKILL.md file and optional references, scripts, and assets. ![Skills Architecture](assets/skills-architecture.svg) @@ -61,6 +61,10 @@ Support tools used throughout. | research-best-practices | Use MCP tools to research approaches | | skill-creator | Package new reusable patterns as skills | | skill-discovery | Search skills.sh for community skills | +| find-skills | Help users discover and install skills for specific tasks | +| aspire | Orchestrate Aspire distributed apps (start, stop, describe, logs) | +| playwright-cli | Automate browser interactions for testing, screenshots, data extraction | +| adr | Generate and manage Architecture Decision Records | ## Brownfield Skills (21) diff --git a/docs/state-and-gates.md b/docs/state-and-gates.md index fde0888..7b38519 100644 --- a/docs/state-and-gates.md +++ b/docs/state-and-gates.md @@ -135,6 +135,8 @@ The most important brownfield gate. After specs are generated, you assess testab - Is there a working dev/test environment? **Outcomes:** -- **Track A** — All/most checked → Green baseline (Gherkin + passing tests) -- **Track B** — Few/none checked → Documentation-only (behavioral docs + manual checklists) -- **Hybrid** — Mixed → Track A for testable features, Track B for the rest +- **Track A** — 5-6 checked → Green baseline (Gherkin + passing tests) +- **Track B** — 0-2 checked → Documentation-only (behavioral docs + manual checklists) +- **Hybrid** — 3-4 checked → Track A for testable features, Track B for the rest + +The track decision must be documented in an ADR with rationale for which checklist items passed/failed.