diff --git a/.agents/skills/api-and-namespace-design/SKILL.md b/.agents/skills/api-and-namespace-design/SKILL.md deleted file mode 100644 index 85b51222..00000000 --- a/.agents/skills/api-and-namespace-design/SKILL.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -name: api-and-namespace-design -description: API design conventions, namespace coordinate system, RBAC roles, ClawHub compatibility layer, OpenAPI contract sync rules, and CSRF/session handling. -license: Apache-2.0 ---- - -# API and Namespace Design Skill - -## Trigger - -Use this skill when: -- Adding or modifying REST API endpoints -- Changing namespace, skill, or user coordinate logic -- Working on ClawHub CLI compatibility layer -- Modifying OpenAPI specifications or generated types -- Adding new admin or governance endpoints - -## Namespace Coordinate System - -SkillHub uses a two-axis coordinate model: - -``` -@{namespace_slug}/{skill_slug} -``` - -- `@global/my-skill` — Global namespace skill -- `@my-team/my-skill` — Team namespace skill (namespace slug is any valid slug) -- `@department-ops/my-skill` — Department namespace skill - -### Namespace Model - -Namespaces (`domain/namespace/`): -- **Slug**: unique identifier, validated by `SlugValidator` -- **Status**: `ACTIVE`, `FROZEN`, `ARCHIVED` -- **Roles**: `OWNER`, `ADMIN`, `MEMBER` -- Frozen or archived namespaces cannot publish skills - -### RBAC Roles - -**Namespace-level** (`domain/namespace/NamespaceRole`): -- `OWNER` — Full control over namespace and all skills -- `ADMIN` — Can manage members, archive skills, publish -- `MEMBER` — Can publish skills to the namespace - -**Platform-level**: -- `SUPER_ADMIN` — Bypasses all permission checks, can publish directly without review - -## ClawHub Compatibility Layer - -ClawHub CLI uses a single-slug model (no `/` allowed in slugs). Mapping: - -| SkillHub Coordinate | Canonical Slug | Notes | -|---------------------|----------------|-------| -| `@global/my-skill` | `my-skill` | Global namespace omits prefix | -| `@team-name/my-skill` | `team-name--my-skill` | Double-dash separator | - -**Conflict resolution**: `--` split takes priority. `@global/team-name--my-skill` would conflict -with `@team-name/my-skill`, resolved to the team namespace skill. Global skill slugs must NOT -contain `--`. - -## API Design - -### Controllers - -- Controllers in `skillhub-app` (`com.iflytek.skillhub.controller/`) are **transport only** -- Responsibilities: extract auth context, bind request params, wrap responses -- Complex business logic belongs in domain services (`skillhub-domain`) or app services -- Use Springdoc OpenAPI annotations (`@Operation`, `@ApiResponse`) for API documentation -- User identity is always **String** in API inputs and outputs - -### Request/Response Patterns - -- DTOs in `com.iflytek.skillhub.dto/` -- `ReviewTaskRequest` / `ReviewTaskResponse` for review workflow -- Response wrapping handled at controller layer -- Validation errors use `DomainBadRequestException` with i18n message keys - -### Session and CSRF - -- Session-based auth with cookie storage -- CSRF protection via `XSRF-TOKEN` cookie and `X-XSRF-TOKEN` header -- Smoke tests validate the full register → login → CSRF → action → logout flow -- Mock auth uses `X-Mock-User-Id` header in local dev - -### Well-known Discovery - -`/.well-known/clawhub.json` returns `{ "apiBase": "/api/v1" }` for ClawHub CLI auto-discovery. - -## OpenAPI Contract Sync - -When backend API contracts change: - -```bash -make generate-api -``` - -This runs `openapi-typescript http://localhost:8080/v3/api-docs -o src/api/generated/schema.d.ts`. - -Commit the updated `web/src/api/generated/schema.d.ts` with the PR. - -To verify no drift: - -```bash -./scripts/check-openapi-generated.sh -``` - -This starts local dependencies, boots the backend, regenerates the schema, and fails if the -checked-in SDK is stale. - -## Versioning and Tags - -- Semantic versioning for skill versions (`major.minor.patch`) -- `latest` tag is system-reserved, read-only, auto-follows `Skill.latestVersionId` -- Custom tags (`stable`, `beta`) are manually maintained -- `latest` cannot be moved manually -- Auto-generated versions use `yyyyMMdd.HHmmss` format when no version is specified in SKILL.md - -## Key API Endpoints - -| Method | Path | Purpose | -|--------|------|---------| -| `GET` | `/api/v1/auth/me` | Current user info (401 if unauthenticated) | -| `POST` | `/api/v1/auth/local/login` | Local account login | -| `POST` | `/api/v1/auth/local/register` | Local account registration | -| `POST` | `/api/v1/auth/logout` | Logout (302/200/204) | -| `POST` | `/api/v1/auth/local/change-password` | Password change | -| `GET` | `/api/v1/namespaces` | List namespaces | -| `GET` | `/api/v1/labels` | List visible labels (public) | -| `POST` | `/api/v1/admin/labels` | Create label definition (admin) | -| `DELETE` | `/api/v1/admin/labels/{slug}` | Delete label definition (admin) | -| `GET` | `/actuator/health` | Health check | -| `GET` | `/actuator/prometheus` | Prometheus metrics | - -## Common Pitfalls - -- Forgetting CSRF token on POST/PUT/DELETE requests (needs `X-XSRF-TOKEN` header) -- Using numeric user IDs in API — all user identities are **String** -- Not regenerating OpenAPI types after adding/changing endpoints -- Putting business logic in controllers instead of domain/app services -- Assuming namespace slugs follow a specific prefix pattern — they are arbitrary valid slugs diff --git a/.agents/skills/backend-module-structure/SKILL.md b/.agents/skills/backend-module-structure/SKILL.md deleted file mode 100644 index ef375e1b..00000000 --- a/.agents/skills/backend-module-structure/SKILL.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: backend-module-structure -description: Rules for the SkillHub backend Maven multi-module clean architecture. Ensures agents place new code in the correct module and respect dependency direction. -license: Apache-2.0 ---- - -# Backend Module Structure Skill - -## Trigger - -Use this skill when: -- Adding or modifying Java backend code -- Creating new services, controllers, repositories, or entities -- Refactoring backend code across files -- Reviewing backend code placement - -## Rules - -### Dependency Direction - -The design-doc dependency direction: - -``` -app → domain, auth, search, storage, infra, notification -infra → domain # implements domain repository interfaces -auth → domain -search → domain -notification → domain -storage → (independent) # pure SPI -``` - -**Design intent**: `skillhub-domain` should be the innermost layer, defining entities, -repository interfaces, and domain services without depending on infra, auth, search, or storage. - -**Code reality**: `skillhub-domain` declares a Maven dependency on `skillhub-storage` (via -`pom.xml`), and several domain services (`SkillHardDeleteService`, `SkillDownloadService`, -`SkillPublishService`, `SkillGovernanceService`, `SkillQueryService`, -`SkillStorageDeletionCompensationService`) import `com.iflytek.skillhub.storage.ObjectStorageService`. -This is an existing deviation from the ideal clean architecture. New code should avoid adding -further cross-module dependencies from domain. - -### Where to Place Code - -| Code Type | Module | Java Package | -|-----------|--------|-------------| -| Entity / Value Object | skillhub-domain | `com.iflytek.skillhub.domain.{submodule}/` | -| Repository Interface | skillhub-domain | `com.iflytek.skillhub.domain.{submodule}/` | -| Domain Service | skillhub-domain | `com.iflytek.skillhub.domain.{submodule}/service/` | -| Domain Event | skillhub-domain | `com.iflytek.skillhub.domain/event/` | -| Domain Exception | skillhub-domain | `com.iflytek.skillhub.domain/shared/exception/` | -| JPA Repository Impl | skillhub-infra | `com.iflytek.skillhub.infra.repository/` | -| Controller | skillhub-app | `com.iflytek.skillhub.controller/` | -| App Service | skillhub-app | `com.iflytek.skillhub.service/` | -| Query Repository | skillhub-app | `com.iflytek.skillhub.repository/` | -| DTO / Response | skillhub-app | `com.iflytek.skillhub.dto/` | -| OAuth2 / Auth Config | skillhub-auth | `com.iflytek.skillhub.auth/` | -| Search SPI / Impl | skillhub-search | `com.iflytek.skillhub.search/` | -| Storage SPI / Impl | skillhub-storage | `com.iflytek.skillhub.storage/` | -| Notification Service | skillhub-notification | `com.iflytek.skillhub.notification/` | - -### Maven Modules - -The parent POM (`server/pom.xml`) defines 7 modules with `spring-boot-starter-parent:3.2.3`: - -``` -skillhub-app | skillhub-domain | skillhub-auth | skillhub-search -skillhub-storage | skillhub-infra | skillhub-notification -``` - -### Repository vs Query Repository - -- **Domain Repository** (`skillhub-domain`): Aggregate reads, state transitions, rule evaluation. - Returns domain objects. Defined as interfaces, implemented in `skillhub-infra` via Spring Data JPA. -- **Query Repository** (`com.iflytek.skillhub.repository`): Read-model assembly, joins multiple - sources, presentation projection. Returns DTOs. Implemented directly in `skillhub-app`. - -Current query repositories: -- `GovernanceQueryRepository` / `JpaGovernanceQueryRepository` -- `MySkillQueryRepository` / `JpaMySkillQueryRepository` -- `ProfileReviewQueryRepository` / `JpaProfileReviewQueryRepository` -- `AdminSkillReportQueryRepository` / `JpaAdminSkillReportQueryRepository` - -When a new read use case arrives: -1. If it's for state transition or domain rule → domain repository port -2. If it's for page/list/detail response assembly with joins → app query repository -3. If it's a thin single-aggregate read → direct domain repository call from app service -4. If direct SQL/EntityManager is needed → add class-level comment explaining why - -### Building Backend Tests - -Never run `./mvnw -pl skillhub-app clean test` directly under `server/`. Use: -```bash -make test-backend-app # skillhub-app + dependencies (includes -am) -make test-backend # all backend modules -``` - -Running clean test on skillhub-app alone can fall back to stale artifacts from the local Maven -repository, surfacing misleading `cannot find symbol` and signature-mismatch errors. - -### User Identity Type - -User identity is **always String** throughout the codebase. This covers: -- Authentication, API params, permissions, audit -- Resource owner, creator, reviewer, actor, submittedBy -- All user-associated fields - -The `UserAccount` entity uses `@Column(length = 128)` for its ID. The platform needs to support -external SSO/OIDC/SCIM identity sources whose UIDs are typically stable strings. diff --git a/.agents/skills/code-conventions/SKILL.md b/.agents/skills/code-conventions/SKILL.md deleted file mode 100644 index 2add2e31..00000000 --- a/.agents/skills/code-conventions/SKILL.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -name: code-conventions -description: Code style, logging, and testing conventions for SkillHub backend (Java) and frontend (TypeScript). Use when writing or reviewing code. -license: Apache-2.0 ---- - -# Code Conventions Skill - -## Java / Backend Conventions - -### User Identity Type - -User identity is **always `String`** throughout the codebase. This covers: -- Authentication and authorization -- API parameters and responses -- Permission checks -- Audit logs -- Resource owner, creator, reviewer, actor, submittedBy fields - -Never introduce `int`, `long`, or `bigint` as user identifiers. The platform needs to support -external SSO/OIDC/SCIM identity sources whose UIDs are typically stable strings. - -### Exception Handling - -- Use `LocalizedDomainException` for user-facing error messages (supports i18n) -- Use `DomainBadRequestException` for invalid client input -- Use `DomainNotFoundException` for missing resources -- Use `DomainForbiddenException` for authorization failures -- Exception classes live in `skillhub-domain/shared/exception/` - -### Domain Services - -- Return domain objects, not DTOs -- Contain business rules and state transitions -- Use domain events for cross-cutting side effects (publishing, notifications) -- Located in `domain/{submodule}/service/` - -### Controllers - -- Transport only: extract auth context, bind request params, wrap responses -- No business logic in controllers -- Located in `com.iflytek.skillhub.controller/` - -### Query Repositories - -- Handle read-model joins and presentation projection -- Return DTOs or presentation models -- Located in `com.iflytek.skillhub.repository/` -- Named like `*QueryRepository` (e.g., `GovernanceQueryRepository`, `MySkillQueryRepository`) - -### App Services - -- Workflow orchestration: coordinate domain services and query repositories -- Should express "what this endpoint does", not "how it assembles DTOs" -- Located in `com.iflytek.skillhub.service/` - -### Logging - -- Use SLF4J with structured logging -- Use MDC for request tracing -- Log at appropriate levels: INFO for business events, DEBUG for troubleshooting, ERROR for failures - -## TypeScript / Frontend Conventions - -### Type Safety - -- Strict TypeScript mode. No `any` types. -- Use generated OpenAPI types from `web/src/api/generated/schema.d.ts` for all API interactions. -- Additional types in `web/src/types/` - -### Data Fetching - -- **Always use TanStack Query** (`@tanstack/react-query`) for server state -- **Never use `useEffect`** for data fetching -- Use `openapi-fetch` client for type-safe API calls - -### Component Composition - -- **Radix UI** primitives: `@radix-ui/react-dropdown-menu`, `@radix-ui/react-select` -- **class-variance-authority** (cva) for component variants -- **clsx** + **tailwind-merge** for class merging -- **`cn()` utility**: `web/src/shared/lib/utils.ts` -- shadcn/ui is NOT used as a library - -### State Management - -- **TanStack Query** for server state (API data, caching, invalidation) -- **Zustand** for local/UI state (theme, sidebar, modals, form state) - -### Feature-Sliced Design - -| Layer | Path | Purpose | -|-------|------|---------| -| Pages | `web/src/pages/` | Route-level page components | -| Features | `web/src/features/` | Self-contained business features | -| Entities | `web/src/entities/` | Domain entity display logic | -| Shared | `web/src/shared/` | Reusable UI components, hooks, utilities | - -Place code at the lowest appropriate layer. Do not put page-level logic in shared. - -### Styling - -- Tailwind CSS for all styling -- Follow existing component patterns -- Use `cn()` for conditional class merging - -### Internationalization - -- Use i18next + react-i18next -- All user-facing text must be translatable -- Translation keys in `web/src/i18n/` - -## Testing Philosophy - -### Backend - -- JUnit 5 + Mockito + AssertJ -- Use Spring Boot test slices where possible (`@WebMvcTest`, `@DataJpaTest`) -- Test behaviors, not implementations -- Use `make test-backend-app` (includes `-am` for dependent modules) -- Never run `./mvnw -pl skillhub-app clean test` directly — stale Maven cache causes misleading errors - -### Frontend - -- Vitest for unit tests -- Playwright for E2E tests -- Test component behavior and user interactions - -## Common Pitfalls - -- **Maven multi-module**: Always use `-am` flag or Makefile targets to include dependent modules -- **OpenAPI types**: Must regenerate and commit after API contract changes -- **String identity**: Never use numeric types for user identifiers -- **Controller business logic**: Move to domain service or app service -- **Complex read-models in app service**: Extract to query repository diff --git a/.agents/skills/dev-workflow/SKILL.md b/.agents/skills/dev-workflow/SKILL.md deleted file mode 100644 index 8f71ee75..00000000 --- a/.agents/skills/dev-workflow/SKILL.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -name: dev-workflow -description: The complete development workflow for SkillHub contributors including local dev, staging validation, testing, and PR creation. Ensures agents follow the correct sequence of steps. -license: Apache-2.0 ---- - -# Development Workflow Skill - -## Trigger - -Use this skill when: -- Starting local development -- Running tests or validation -- Preparing a pull request -- Setting up the development environment -- Working with parallel agent worktrees - -## Prerequisites - -- Java 21+ (`java -version`) -- Maven wrapper (`./mvnw` in `server/`) -- Node.js + pnpm -- Docker + docker compose -- `gh` CLI (for PR creation) -- `curl` (for smoke tests and health checks) - -## Workflow Stages - -### Stage 1: Local Development (fast iteration) - -**One-command start:** - -```bash -make dev-all # Start full stack: Postgres, Redis, MinIO, scanner, backend, frontend -make dev-all-down # Stop everything -make dev-all-reset # Full reset (clears data volumes) -make dev-status # Check service status -``` - -**Access points:** -- Web UI: `http://localhost:3000` -- Backend API: `http://localhost:8080` -- Scanner: `http://localhost:8000` - -**Individual components:** - -```bash -make dev # Start dependency services only (Postgres, Redis, MinIO, scanner) -make dev-server # Start backend in foreground (blocking) -make dev-web # Start Vite dev server (HMR enabled) -make dev-server-restart # Restart backend process -make dev-down # Stop dependency services -make dev-logs # View backend logs (use SERVICE=frontend for frontend logs) -``` - -**Backend development**: After editing Java code, run `make dev-server-restart`. - -**Frontend development**: Vite HMR enabled — save a file for instant browser updates. - -**Scanner**: The security scanner is enabled by default in dev. Health checked at `http://localhost:8000/health`. - -### Stage 2: Testing - -| Command | Scope | Notes | -|---------|-------|-------| -| `make test-backend-app` | Backend unit tests | skillhub-app + dependencies (`-am`) | -| `make test-backend` | All backend modules | All modules via `./mvnw test` | -| `make test-frontend` | Frontend unit tests | Vitest (pnpm run test) | -| `make test-e2e-frontend` | Frontend E2E tests | Playwright | -| `make test-e2e-smoke-frontend` | Frontend E2E smoke | Playwright subset | -| `make typecheck-web` | TypeScript type check | `tsc --noEmit` | -| `make lint-web` | ESLint check | Frontend linting | - -**Important**: Never run `./mvnw -pl skillhub-app clean test` directly under `server/`. -Use `-am` or Makefile targets to include dependent modules. - -### Stage 3: Staging Regression (pre-PR validation) - -```bash -make staging # Build backend Docker image + frontend static + smoke test -make staging-down # Tear down -make staging-logs # View backend logs -SERVICE=web make staging-logs # View Nginx logs -``` - -Staging validates the containerized deployment path: -- Backend: built as Docker image from local source (`Dockerfile.dev`) -- Frontend: built as static files (`pnpm build`), served by Nginx -- Dependencies: same Postgres/Redis/MinIO as local dev -- Smoke test runs against staging via `scripts/smoke-test.sh` - -**Staging URLs:** -- Web UI: `http://localhost` -- Backend API: `http://localhost:8080` - -**Staging credentials** (for bootstrap admin): -- Username: `admin` -- Password: `Admin@staging2026` - -### Stage 4: Pull Request - -```bash -make pr # Push branch + create PR (requires gh CLI) -``` - -Requirements: -- `gh` CLI installed and authenticated -- Not on main/master branch -- All changes committed (will prompt if uncommitted changes exist) - -### Useful Commands - -| Command | Description | -|---------|-------------| -| `make generate-api` | Regenerate OpenAPI types from running backend | -| `make namespace-smoke` | Namespace workflow smoke test | -| `make db-reset` | Reset database only (Flyway migrate) | -| `make validate-release-config` | Validate release env vars (.env.release) | -| `./scripts/smoke-test.sh` | Basic API smoke test (health, auth, labels) | -| `./scripts/namespace-smoke-test.sh` | Namespace CRUD + membership smoke test | -| `./scripts/check-openapi-generated.sh` | Verify OpenAPI types are not stale | -| `make parallel-init TASK=name` | Create parallel worktree for agent | - -### Mock Auth Users - -| User ID | Role | Header | -|---------|------|--------| -| `local-user` | Regular user | `X-Mock-User-Id: local-user` | -| `local-admin` | Super admin | `X-Mock-User-Id: local-admin` | - -Bootstrap admin (local profile): -- Username: `admin` -- Password: `ChangeMe!2026` - -### Smoke Test Coverage - -`scripts/smoke-test.sh` validates: -1. Health endpoint (`/actuator/health` → 200) -2. Prometheus metrics (`/actuator/prometheus` → 200) -3. Namespaces API (`/api/v1/namespaces` → 200) -4. Auth required (`/api/v1/auth/me` → 401 without session) -5. User registration flow (with CSRF) -6. Auth me with session -7. Password change -8. Logout + verify 401 after -9. Admin login -10. Label CRUD (admin only) - -Additional smoke tests: -- `scripts/namespace-smoke-test.sh` — Namespace creation, membership, publishing -- `scripts/governance-smoke-test.sh` — Governance and moderation -- `scripts/promotion-smoke-test.sh` — Skill promotion between scopes - -### Parallel Agent Workflow - -For parallel agent development with isolated worktrees: - -```bash -make parallel-init TASK=feature-name # Create worktree -make parallel-sync SOURCES="feat1 feat2" # Merge feature branches -make parallel-up SOURCES="feat1 feat2" # Merge + start dev environment -make parallel-down # Stop parallel environment -``` - -See `docs/13-parallel-workflow.md` for full details. - -### Commit Style - -Use conventional commit format: - -``` -(): -``` - -Examples: -``` -feat(auth): add local account login -fix(publish): resolve null pointer when skill metadata is missing name -docs(deploy): clarify runtime image usage -test(namespace): add membership service edge case tests -refactor(review): extract query repository for governance list -chore(ci): add parallel workflow scripts -``` - -### Common Issues - -| Issue | Solution | -|-------|----------| -| Backend won't start | Check Java version (`java -version`), must be 21+ | -| Port 8080 in use | `lsof -i :8080` to find and kill the process | -| Maven download timeout | Configure mirror in `~/.m2/settings.xml` | -| Frontend won't start | Run `make web-deps` to ensure node_modules exist | -| Staging build fails | Check `Dockerfile.dev` and ensure Maven build succeeds first | -| CSRF errors in tests | Ensure cookie jar is shared and CSRF token refreshed after login | diff --git a/.agents/skills/frontend-conventions/SKILL.md b/.agents/skills/frontend-conventions/SKILL.md deleted file mode 100644 index be7e647e..00000000 --- a/.agents/skills/frontend-conventions/SKILL.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: frontend-conventions -description: Coding conventions, architecture patterns, and testing rules for the SkillHub React frontend. Ensures agents follow Feature-Sliced Design and use the generated OpenAPI types. -license: Apache-2.0 ---- - -# Frontend Conventions Skill - -## Trigger - -Use this skill when: -- Adding or modifying React/TypeScript frontend code -- Creating new pages, features, entities, or shared components -- Changing API client calls or data fetching patterns - -## Rules - -### Feature-Sliced Design - -Place code at the lowest appropriate layer: - -| Layer | Path | Purpose | -|-------|------|---------| -| Pages | `web/src/pages/` | Route-level page components | -| Features | `web/src/features/` | Business features (search, upload, review, etc.) | -| Entities | `web/src/entities/` | Domain entity display logic (skill, user, namespace) | -| Shared | `web/src/shared/` | Reusable UI components, hooks, utilities | - -Current features: -- `admin` — Admin panel (user management, labels, search) -- `auth` — Login, OAuth flows, device auth -- `governance` — Skill governance actions (hide, yank, archive) -- `namespace` — Namespace management (members, settings) -- `notification` — User notifications and inbox -- `promotion` — Skill promotion between scopes -- `publish` — Skill upload/publish UI -- `report` — Skill reporting -- `review` — Review workflow UI -- `search` — Skill search and filtering -- `security-audit` — Security audit viewer -- `skill` — Skill detail, listing, cards -- `social` — Stars, ratings, subscriptions -- `token` — API token management - -### Data Fetching - -- **Always use TanStack Query** (`@tanstack/react-query`) for server state. -- **Never use `useEffect`** for data fetching. -- Use `openapi-fetch` client with generated types from `web/src/api/generated/schema.d.ts`. -- Never use `any` types. - -### State Management - -- **TanStack Query** for server state (API data, caching, invalidation, optimistic updates) -- **Zustand** for local/UI state (theme, sidebar, modals, form state) - -### Component Composition - -- **Radix UI** primitives: `@radix-ui/react-dropdown-menu`, `@radix-ui/react-select` -- **class-variance-authority** (cva) for component variants -- **clsx** + **tailwind-merge** for class merging -- **`cn()` utility**: `web/src/shared/lib/utils.ts` -- **shadcn/ui is NOT used** as a library - -### API Type Generation - -When backend OpenAPI contracts change: - -```bash -make generate-api -``` - -This runs `openapi-typescript http://localhost:8080/v3/api-docs -o src/api/generated/schema.d.ts`. - -Commit the updated `web/src/api/generated/schema.d.ts` with the PR. - -To verify the generated file is not stale: - -```bash -./scripts/check-openapi-generated.sh -``` - -### Styling - -- **Tailwind CSS** for all styling -- **`cn()` utility** for conditional class merging -- Follow existing component patterns in `web/src/shared/components/` - -### Internationalization - -- **i18next** + **react-i18next** for translations -- All user-facing text must be translatable -- Translation keys in `web/src/i18n/` - -### Build & Development - -```bash -make dev-web # Start Vite dev server (HMR enabled) -make build-frontend # Production build -make typecheck-web # TypeScript type check (tsc --noEmit) -make lint-web # ESLint check -make test-frontend # Vitest unit tests -make test-e2e-frontend # Playwright E2E tests -make test-e2e-smoke-frontend # Playwright smoke tests -``` - -Vite HMR is enabled by default — save a file and the browser updates instantly. - -### Frontend Dependencies - -Key dependencies (from `web/package.json`): -- `react` 19, `react-dom` 19 -- `@tanstack/react-query` 5 -- `@tanstack/react-router` 1 -- `@radix-ui/react-dropdown-menu`, `@radix-ui/react-select` -- `class-variance-authority`, `clsx`, `tailwind-merge` -- `openapi-fetch` 0.13 -- `i18next`, `react-i18next` -- `zustand` 5 -- `react-markdown`, `rehype-highlight`, `rehype-sanitize` -- `lucide-react` (icons) -- `sonner` (toasts) - -Build tools: Vite 6, TypeScript 5.7, Vitest 3.2, Playwright 1.58 diff --git a/.agents/skills/pr-submission/SKILL.md b/.agents/skills/pr-submission/SKILL.md deleted file mode 100644 index d1b889af..00000000 --- a/.agents/skills/pr-submission/SKILL.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: pr-submission -description: PR title format, commit conventions, and pre-PR checklist for SkillHub. Use when preparing or reviewing pull requests. -license: Apache-2.0 ---- - -# PR Submission Skill - -## Workflow - -1. Identify the scope of your change (feature, bug fix, docs, test, refactor, chore) -2. Format PR title and commits using the conventions below -3. Run the pre-PR checklist commands -4. Open the PR with a descriptive body - -## PR Title Format - -Use conventional commit style: - -``` -(): -``` - -**Types:** - -| Type | When to Use | -|------|-------------| -| `feat` | New feature or capability | -| `fix` | Bug fix | -| `docs` | Documentation changes only | -| `test` | Adding or updating tests | -| `refactor` | Code restructuring with no behavior change | -| `chore` | Build, CI, tooling, or maintenance tasks | - -**Scopes:** Use module or domain names: `auth`, `search`, `publish`, `review`, `namespace`, `governance`, `deploy`, `ci`, `frontend`, `scanner` - -**Examples:** -``` -feat(auth): add local account login with password reset -fix(publish): resolve null pointer when skill metadata is missing name -docs(deploy): clarify runtime image usage -test(namespace): add membership service edge case tests -refactor(review): extract query repository for governance list -chore(ci): add parallel workflow scripts for multi-agent development -``` - -## Commit Message Format - -Same convention as PR titles. One logical change per commit. - -**Types:** - -- **feat**: A new feature for the user -- **fix**: A bug fix for the user -- **docs**: Documentation changes only -- **test**: Adding or updating tests -- **refactor**: Code change that neither fixes a bug nor adds a feature -- **chore**: Changes to build process, CI, or maintenance tasks - -**Examples:** -``` -fix(auth): resolve session cookie conflict in device flow -feat(publish): support security scan before review submission -docs(skill-protocol): add nested SKILL.md discovery rules -test(search): verify jieba analysis with Chinese skill descriptions -refactor(storage): simplify LocalFile path normalization -``` - -## Pre-PR Checklist - -- [ ] Backend tests pass: `make test-backend-app` -- [ ] Frontend typecheck passes: `make typecheck-web` -- [ ] If API changed: `make generate-api` was run and `web/src/api/generated/schema.d.ts` is committed -- [ ] Smoke test passes: `make staging` -- [ ] Follow existing module boundaries and dependency direction -- [ ] Add/update tests for new behavior -- [ ] Update design docs when APIs, auth flows, deployment, or operator workflows change - -## PR Body Structure - -When creating a PR, include: - -1. **What** — Summary of the change -2. **Why** — Motivation (link to issue if applicable) -3. **How** — Key implementation details (especially for non-obvious decisions) -4. **Testing** — How to verify the change works -5. **Impact** — Breaking changes, migration notes, or rollout considerations - -## Review Conventions - -- When reviewing, cite the specific AGENTS.md rule that applies if suggesting a convention change -- For backend code, check dependency direction does not violate clean architecture rules -- For frontend code, check OpenAPI types are regenerated if API changed diff --git a/.agents/skills/skill-lifecycle/SKILL.md b/.agents/skills/skill-lifecycle/SKILL.md deleted file mode 100644 index 8234b479..00000000 --- a/.agents/skills/skill-lifecycle/SKILL.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -name: skill-lifecycle -description: The authoritative skill lifecycle state model including container states, version states, review workflow states, visibility overlay, and governance actions. Ensures agents don't introduce invalid states or transitions. -license: Apache-2.0 ---- - -# Skill Lifecycle Skill - -## Trigger - -Use this skill when: -- Modifying skill publish, review, or unpublish flows -- Adding or changing skill/version status fields -- Working on search, detail pages, or listing pages that show skill state -- Implementing governance actions (hide, yank, archive) -- Adding new state transitions or permission checks - -## State Model - -### Skill Container States - -Enum `SkillStatus` (`domain/skill/SkillStatus.java`): - -| Value | Meaning | -|-------|---------| -| `ACTIVE` | Skill is operational and can have versions published | -| `HIDDEN` | Skill hidden by platform governance (design doc says prefer boolean `hidden` flag instead) | -| `ARCHIVED` | Skill archived by owner/namespace admin, cannot publish new versions | - -**Design-vs-code note**: `docs/14-skill-lifecycle.md` specifies `hidden` should be a governance -overlay (boolean flag) rather than a lifecycle enum state. The current code still defines -`SkillStatus.HIDDEN`. New code should use the `skill.hidden` boolean field, not the enum value. - -### SkillVersion States - -Enum `SkillVersionStatus` (`domain/skill/SkillVersionStatus.java`): - -| Value | Meaning | -|-------|---------| -| `DRAFT` | Non-public draft, can resubmit or delete | -| `SCANNING` | Undergoing security scan | -| `SCAN_FAILED` | Security scan failed | -| `UPLOADED` | Uploaded but not yet submitted for review (or withdrawn from review) | -| `PENDING_REVIEW` | Frozen pending reviewer action | -| `PUBLISHED` | Currently distributable | -| `REJECTED` | Review denied, retained | -| `YANKED` | Was published, withdrawn from distribution | - -### ReviewTask States - -Enum `ReviewTaskStatus` (`domain/review/ReviewTaskStatus.java`): - -| Value | Meaning | -|-------|---------| -| `PENDING` | Awaiting reviewer | -| `APPROVED` | Reviewer approved | -| `REJECTED` | Reviewer rejected | - -### Visibility Model - -Enum `SkillVisibility` (used in `SkillPublishService`): - -| Value | Publish Path | -|-------|-------------| -| `PUBLIC` | Creates `PENDING_REVIEW` version, review task, security scan | -| `NAMESPACE_ONLY` | Same as PUBLIC but limited visibility scope | -| `PRIVATE` | Goes directly to `UPLOADED` status, no review task | - -`SUPER_ADMIN` role bypasses review — versions go directly to `PUBLISHED`. - -### Latest Version Pointer - -`Skill.latestVersionId` is **only** the latest published pointer: -- Can only point to a `PUBLISHED` version -- May be `null` if no published version exists -- `latest` tag auto-follows this pointer (read-only) -- When yanking: recalculates to newest remaining `PUBLISHED` version, or `null` - -### Key Transitions - -| Action | From | To | Notes | Source | -|--------|------|-----|-------|--------| -| First upload (PUBLIC/NAMESPACE_ONLY) | — | `PENDING_REVIEW` | Review task created | `SkillPublishService` | -| First upload (SUPER_ADMIN) | — | `PUBLISHED` | Direct publish, `SkillPublishedEvent` emitted | `SkillPublishService` | -| First upload (PRIVATE) | — | `UPLOADED` | No review task, `latestVersionId` updated | `SkillPublishService` | -| Review approve | `PENDING_REVIEW` | `PUBLISHED` | Updates `latestVersionId` | Review workflow | -| Review reject | `PENDING_REVIEW` | `REJECTED` | Version retained | Review workflow | -| Withdraw review | `PENDING_REVIEW` | `UPLOADED` | Deletes pending `ReviewTask` | `SkillGovernanceService.withdrawPendingVersion` | -| Yank | `PUBLISHED` | `YANKED` | Recalculates `latestVersionId` | `SkillGovernanceService.yankVersion` | -| Hide | — | `hidden=true` | Independent overlay | `SkillGovernanceService.hideSkill` | -| Restore | — | `hidden=false` | Independent overlay | `SkillGovernanceService.unhideSkill` | -| Archive | `ACTIVE` | `ARCHIVED` | `SkillStatusChangedEvent` emitted | `SkillGovernanceService.archiveSkill` | -| Unarchive | `ARCHIVED` | `ACTIVE` | `SkillStatusChangedEvent` emitted | `SkillGovernanceService.unarchiveSkill` | -| New publish (existing pending) | `PENDING_REVIEW` | `UPLOADED` | Auto-withdraw + delete review task | `SkillPublishService` | -| Delete version | `DRAFT`/`REJECTED`/`SCAN_FAILED`/`UPLOADED` | — | Last version protected | `SkillGovernanceService.deleteVersion` | - -### Yank Pointer Recalculation - -When yanking the current `latestVersionId` (`SkillGovernanceService`): -1. Query all remaining `PUBLISHED` versions for the skill -2. Sort by `publishedAt` DESC, then `createdAt` DESC, then `id` DESC -3. Point `latestVersionId` to the top result, or `null` if none remain - -### Lifecycle Projection - -Read models (detail, my-skills, favorites, search) use `*QueryRepository` patterns: -- `headlineVersion` — Main display version for the page -- `publishedVersion` — Latest published version -- `ownerPreviewVersion` — Pending review version (visible to owner/namespace admin) -- `resolutionMode` — `PUBLISHED`, `OWNER_PREVIEW`, or `NONE` - -**Public browsing, install, download, search only use `publishedVersion`.** - -### Permission Boundaries - -| Action | Who | -|--------|-----| -| Withdraw review | Submitter only | -| Delete version | Owner or namespace admin, only `DRAFT`/`REJECTED`/`SCAN_FAILED`/`UPLOADED` | -| Archive/unarchive | Owner or namespace admin (`ADMIN` or `OWNER` role) | -| Hide/restore | Platform governance (no permission check in code) | -| Yank | Platform governance (no permission check in code) | -| Publish PUBLIC skill | Namespace member (or `SUPER_ADMIN`) | -| Publish PRIVATE skill | Namespace member (or `SUPER_ADMIN`) | - -### Delete Version Constraints - -`SkillGovernanceService.deleteVersion` enforces: -- Only `DRAFT`, `REJECTED`, `SCAN_FAILED`, or `UPLOADED` versions can be deleted -- Cannot delete the last remaining version of a skill -- Deletes associated storage keys (individual files + `bundle.zip`) -- Deletes associated security scan records -- Updates `latestVersionId` if the deleted version was the pointer -- Storage deletion happens after transaction commit with compensation recording - -### Domain Events - -| Event | When Emitted | -|-------|-------------| -| `SkillStatusChangedEvent` | Archive or unarchive | -| `SkillPublishedEvent` | SUPER_ADMIN direct publish | -| `SkillVersionYankedEvent` | Yank action | -| `ReviewSubmittedEvent` | Create review task for PUBLIC/NAMESPACE_ONLY | - -### Common Pitfalls - -- Setting `SkillStatus.HIDDEN` directly — use `skill.setHidden(true)` via `SkillGovernanceService` instead -- Forgetting to recalculate `latestVersionId` after yank or version deletion -- Not auto-withdrawing pending versions when publishing a new version -- Missing the `confirmWarnings` two-step publish flow (warnings require explicit confirmation) -- Assuming all publish flows create review tasks — `PRIVATE` visibility skips review diff --git a/.agents/skills/testing-and-ci/SKILL.md b/.agents/skills/testing-and-ci/SKILL.md deleted file mode 100644 index d08a3669..00000000 --- a/.agents/skills/testing-and-ci/SKILL.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -name: testing-and-ci -description: Testing conventions, CI pipeline rules, and smoke test coverage for SkillHub. Ensures agents write tests correctly and understand the CI gate requirements. -license: Apache-2.0 ---- - -# Testing and CI Skill - -## Trigger - -Use this skill when: -- Adding or modifying backend tests -- Adding or modifying frontend tests -- Changing CI/CD workflows -- Adding smoke tests or E2E tests - -## Rules - -### Backend Testing - -Tests live alongside source in each module's `src/test/java/`: -- `server/skillhub-app/src/test/java/` — Controller integration tests, service tests -- `server/skillhub-domain/src/test/java/` — Domain service unit tests -- `server/skillhub-auth/src/test/java/` — Auth flow tests - -**Tools**: JUnit 5 + Mockito + AssertJ + Spring Boot test slices (`@WebMvcTest`, `@DataJpaTest`) - -**Build commands:** -```bash -make test-backend-app # skillhub-app + dependencies (includes -am) -make test-backend # all backend modules -``` - -**Never** run `./mvnw -pl skillhub-app clean test` directly under `server/`. -`skillhub-app` depends on sibling modules, and a standalone clean build can fall back to stale -artifacts from the local Maven repository, surfacing misleading `cannot find symbol` and -signature-mismatch errors. Use `-am`, or the Makefile targets above. - -**Test naming conventions:** -- Controller tests: `{ControllerName}Test.java` (e.g., `SkillControllerTest.java`) -- Service tests: `{ServiceName}Test.java` -- Integration tests: `{FlowName}IntegrationTest.java` -- Security tests: `{ControllerName}SecurityTest.java` - -### Frontend Testing - -**Tools**: Vitest (unit), Playwright (E2E) - -```bash -make test-frontend # Vitest unit tests (pnpm run test) -make test-e2e-frontend # Playwright E2E tests -make test-e2e-smoke-frontend # Playwright smoke tests (subset) -``` - -E2E tests live in `web/e2e/`. - -### Smoke Tests - -Smoke tests validate end-to-end operator workflows against a running backend: - -| Script | Purpose | -|--------|---------| -| `scripts/smoke-test.sh` | Basic API health, auth, label CRUD | -| `scripts/namespace-smoke-test.sh` | Namespace creation, membership, publishing | -| `scripts/governance-smoke-test.sh` | Governance and moderation flows | -| `scripts/promotion-smoke-test.sh` | Skill promotion between scopes | - -When operator-facing workflows change, update the corresponding smoke test. - -### CI Pipeline - -GitHub Actions workflows in `.github/workflows/`: - -| Workflow | Trigger | Purpose | -|----------|---------|---------| -| `pr-tests.yml` | PR | Backend + frontend unit tests | -| `pr-e2e.yml` | PR | E2E smoke tests against staging | -| `pr-batch-test-deploy.yml` | workflow_dispatch | Batch test and deploy | -| `publish-images.yml` | release published / workflow_dispatch | Build and publish Docker images to GHCR | -| `deploy-docs.yml` | push to docs | Deploy documentation site | -| `issue-triage.yml` | issues | Auto-triage incoming issues | -| `issue-backlog-rescore.yml` | cron (every 6h) | Rescore backlog issues | -| `release-notes.yml` | workflow_dispatch | Generate release notes | -| `deepwiki.yml` | release published | Update DeepWiki documentation | -| `claim-issue-reward.yml` | issue_comment | Auto-claim issue rewards | -| `statistic-member-reward.yml` | cron/schedule | Calculate member rewards | - -All workflows live in `.github/workflows/`. Deno scripts for triage, release notes, and rewards -live in `.github/scripts/`. - -### Staging - -Before opening a PR, validate with staging: - -```bash -make staging # Build backend Docker image + frontend static + smoke test -make staging-down # Tear down -SERVICE=web make staging-logs # View Nginx logs -``` - -Staging validates the containerized deployment path: -- Backend: built as Docker image from local source (`Dockerfile.dev`) -- Frontend: built as static files (`pnpm build`), served by Nginx -- Dependencies: same Postgres/Redis/MinIO as local dev - -If staging passes, the environment stays running at: -- Web UI: `http://localhost` -- Backend API: `http://localhost:8080` - -### Pre-PR Testing Checklist - -- [ ] `make test-backend-app` passes -- [ ] `make typecheck-web` passes -- [ ] `make lint-web` passes (if frontend changed) -- [ ] `make staging` passes (full regression) -- [ ] If API changed: `make generate-api` run and generated file committed -- [ ] New behavior has corresponding tests diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1e9094b2..8954d516 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,8 +5,9 @@ ## Validation -- [ ] Backend tests passed -- [ ] Frontend typecheck/build passed +- [ ] Applicable checks selected from `.trellis/spec/testing/index.md`; exclusions explained +- [ ] Backend tests passed when backend behavior changed +- [ ] Frontend typecheck/build passed when frontend files changed - [ ] OpenAPI SDK regenerated or checked when API contracts changed - [ ] Smoke test run when relevant diff --git a/.gitignore b/.gitignore index a54e5768..42f557d9 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,14 @@ docs/superpowers/ # Local workspace metadata CLAUDE.md +# Share stable engineering specs; keep Trellis execution state local. +!/.trellis/ +/.trellis/* +!/.trellis/spec/ +!/.trellis/spec/** +/.agents/skills/trellis-*/ +/AGENTS.override.md + # Local report-generation skill .agents/skills/generate-skillhub-weekly-report/ diff --git a/.trellis/spec/backend/api-contracts.md b/.trellis/spec/backend/api-contracts.md new file mode 100644 index 00000000..4e4c09e3 --- /dev/null +++ b/.trellis/spec/backend/api-contracts.md @@ -0,0 +1,43 @@ +# API 与行为契约维护 + +适用后端、前端和 CLI 的公共接口变更;消费者可直接读本页,无需加载整个后端目录。 +具体端点、坐标、RBAC、版本标签和兼容语义以 [API设计](../../../docs/06-api-design.md)、 +[领域模型](../../../docs/02-domain-model.md)、[生命周期](../../../docs/14-skill-lifecycle.md)及相关 OpenSpec 为正式来源。 +不在 Trellis 复制端点/角色/状态表。 + +身份、会话、设备授权或 API token 变化追加[认证设计](../../../docs/03-authentication-design.md)及对应实现测试。 +token 持久化保留现有安全哈希机制,不退化为明文存储,不把展示前缀当作凭证本体。 +发布、上传、解压或包校验变化追加[包协议](../../../docs/07-skill-protocol.md),核对实际校验策略; +不在此复制易过期的扩展名与大小限制表,拒绝路径和边界值应有测试。 + +## 工程约定 + +- DTO 放 app 的 dto 包,Controller 负责参数绑定、认证上下文与响应包装;业务规则由领域/应用服务承担。 +- 用户身份使用 String。客户端传入 owner/user 字段不等于已认证操作者,授权需使用可信身份上下文。 +- 普通 JSON 接口沿用 ApiResponse/ApiResponseFactory,文案通过现有本地化机制生成;流式文件和兼容接口按已有协议处理,不能强行套壳。 +- 分页字段、页码基数、排序、空值、缺省值与可选字段逐项核对端点和消费者;不凭框架默认值推导公共协议。 +- 参数格式、权限与领域前置状态分别校验;HTTP 错误处理见 [错误专题](./error-handling.md)。 +- 同一修改说明成功后的状态、失败后的状态、可重试条件和副作用;取消 HTTP 等待不自动意味着服务端事务取消。 + +## 身份与兼容边界 + +Session/CSRF 行为核对 [SecurityConfig](../../../server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java) 与相应认证链。 +浏览器会话使用的 XSRF-TOKEN/X-XSRF-TOKEN 与 token/兼容接口的例外不能混为一条“所有写请求都带 CSRF”。 +Mock 身份仅按现有开发 profile 使用,不替代真实认证验收。 +命名空间 slug、二轴坐标到 ClawHub 单 slug 的解析、冲突优先级和 well-known discovery 变更时,同时检查 compat 与 CLI 消费方。 +不要根据 namespace 名称前缀猜测类型或权限。 + +## 契约变更验收 + +1. 明确受影响消费者、字段可选性、错误结构与兼容策略;不能因类型仍编译就认定行为兼容。 +2. 保持 Controller/OpenAPI 注解和 DTO 一致;从正确运行中的后端执行 `make generate-api`,检查生成 diff 并随契约提交,勿手改生成类型。 +3. 选择 HTTP/消费方测试验证实际响应和失败路径;至少覆盖此次变化触及的身份/权限边界。 +4. [生成漂移脚本](../../../scripts/check-openapi-generated.sh) 会启动依赖并在退出时执行 Compose down;仅在隔离、可清理的本地环境运行,不能作为无副作用的文档检查。 + +## 已知文档漂移 + +2026-10-15 静态核查:docs/14 的撤回描述仍为 DRAFT,而 +[withdrawPendingVersion 实现](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java) 设置 UPLOADED。 +旧工程 Skill 的状态表也不是新的权威。修改相关功能时对照 +[治理测试](../../../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java)和正式需求解决差异;本次迁移不裁定新业务状态。 +发布审核绕过、可见性、latest 指针、警告确认、删除约束均须核对当前服务与测试,不从历史快照复制规则。 diff --git a/.trellis/spec/backend/database-guidelines.md b/.trellis/spec/backend/database-guidelines.md new file mode 100644 index 00000000..2251b4d1 --- /dev/null +++ b/.trellis/spec/backend/database-guidelines.md @@ -0,0 +1,29 @@ +# 数据库、查询与迁移 + +适用 PostgreSQL/JPA/Flyway 代码。数据模型见 [领域模型](../../../docs/02-domain-model.md), +数据库配置及版本核对实际构建和部署配置。 + +## 仓储与读模型 + +- 聚合读取、状态迁移和规则判断用 domain 仓储接口,由 infra 实现。 +- 页面列表、join、DTO 投影用 app 查询仓储;薄的单聚合读取不必新增查询抽象。 +- 必要的直接 SQL/EntityManager 在类注释解释原因,参数绑定避免拼接用户输入。 +- 修改分页、排序、过滤或计数时同时检查数据查询与 total 查询,以及权限过滤;关注稳定排序、空结果、同值排序与跨页重复。 +- 数据库特性、索引和锁行为用真实 PostgreSQL 验证,mock 返回成功不证明 SQL 可执行。 + +## 迁移约定 + +结构变更使用 [Flyway迁移](../../../server/skillhub-app/src/main/resources/db/migration/)。 +已发布迁移不靠改旧文件纠正;新增迁移需考虑现有数据、默认值、回填、约束和新旧应用兼容。 +新建库成功不能证明存量升级成功;影响已有数据时验证代表性旧数据升级。 +用户身份列保持字符串语义。结构完整性与多变的业务政策分开:新增 CHECK 等约束需说明稳定性、迁移代价及与应用规则的关系,不能把全部工作流写死在 SQL 中。 + +变更查询前定位实际慢点与调用频率,检查 N+1、查询范围和必要索引;性能结论提供代表性数据与执行计划,不能从“加了索引”直接推导收益。 +涉及缓存时说明权威数据来源、失效触发和身份/命名空间隔离,不以缓存替代授权。 + +## 验证与边界 + +状态竞争、事务回滚见 [事务与并发](./transactions-and-concurrency.md)。 +迁移发布顺序和回退限制读 [部署规范](../deployment/index.md);测试方法读 [后端测试](../testing/backend-testing.md)。 +例证:[查询实现](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/JpaMySkillQueryRepository.java)、 +[查询测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/JpaMySkillQueryRepositoryTest.java)。 diff --git a/.trellis/spec/backend/directory-structure.md b/.trellis/spec/backend/directory-structure.md new file mode 100644 index 00000000..f6c3c469 --- /dev/null +++ b/.trellis/spec/backend/directory-structure.md @@ -0,0 +1,35 @@ +# 模块与目录约定 + +适用类归属、模块依赖和读写模型划分。架构目的见 [系统架构](../../../docs/01-system-architecture.md)。 + +| 职责 | 模块及包 | +|---|---| +| 实体、值对象、领域仓储接口 | `server/skillhub-domain`,`domain/{submodule}` | +| 领域服务、事件、本地化异常 | `domain/{submodule}/service`、`domain/event`、`domain/shared/exception` | +| 持久化适配器 | `server/skillhub-infra`,`infra/jpa`、`infra/repository`(跟随现有实现) | +| Controller、DTO、应用服务 | `server/skillhub-app`,`controller`、`dto`、`service` | +| 展示投影、跨来源查询 | `skillhub-app`,`repository/*QueryRepository` | +| 认证授权、搜索、存储、通知 | 对应 `skillhub-auth/search/storage/notification` 模块 | + +包前缀为 `com.iflytek.skillhub`。沿现有业务子域命名,不按表格批量移动存量代码。 +包应通过 `package-info.java` 说明职责;新增包补齐,存量缺失不借本次改动批量补写或搬迁。 + +## 依赖与职责 + +设计方向为 app 装配各模块,infra 实现 domain 仓储接口,auth/search/notification 使用领域模型。 +Controller 绑定参数、提取可信身份、包装响应,不直接做领域状态迁移。 +应用服务表达用例编排;领域服务返回领域对象,不返回 app DTO。 +复杂 join、快照与展示投影放查询仓储;简单单聚合读取可以直接调用领域仓储,避免空壳转发层。 +领域事件用于已有跨模块副作用协作;是否提交后处理、是否需要持久化投递,读 [事务专题](./transactions-and-concurrency.md),不能仅靠事件名称判断可靠性。 + +## 已知偏差与审查 + +[domain POM](../../../server/skillhub-domain/pom.xml) 仍依赖 storage;这是与理想内层隔离的存量偏差。 +本地修改不顺带修复整个架构,也不据此继续增加 domain 到 infra/auth/search/storage 的依赖。 +新增跨模块访问检查 POM 和 import;业务确需改变边界时走设计评审并记录原因。 + +用户身份在认证、API、权限、审计与 owner/reviewer/actor 字段保持 String,依据 [领域模型](../../../docs/02-domain-model.md)。 +不要推广为所有资源 ID 都是 String。 + +例证:[查询仓储](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/JpaMySkillQueryRepository.java)、 +[领域仓储适配器](../../../server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillStorageDeletionCompensationRepositoryAdapter.java)。 diff --git a/.trellis/spec/backend/error-handling.md b/.trellis/spec/backend/error-handling.md new file mode 100644 index 00000000..10e6fe55 --- /dev/null +++ b/.trellis/spec/backend/error-handling.md @@ -0,0 +1,36 @@ +# 错误处理 + +适用领域失败、HTTP 映射和面向用户的错误信息。 + +- 领域失败沿用 `LocalizedDomainException` 及现有 bad-request/not-found/forbidden 子类,携带消息键与安全参数;不要在领域层返回 HTTP DTO。 +- 认证链、Controller advice 和兼容层各按已有协议处理,不能假设所有错误必然经过同一个 advice。 +- 复用 ApiResponseFactory 和现有消息资源。面向用户的业务提示可本地化,内部异常详情不直接拼进响应。 +- 不用异常文案匹配来新增业务分类。发现存量特殊兼容处理时保持其适用范围,避免扩展成通用规则。 +- 错误响应验证 HTTP 状态、响应结构、消息与失败后数据状态;只断言“抛了异常”不足以证明事务回滚。 +- 不吞掉基础设施失败并返回业务成功;是否重试取决于错误类别及重复执行安全性。 + +当前统一 handler 将乐观锁冲突映射为 409、对象存储访问故障映射为 503,并隐藏内部错误细节。 +这些是实现锚点,不代表所有失败都应转换成同一状态码。 + +来源:[异常实现](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/GlobalExceptionHandler.java)、 +[响应与脱敏断言](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/exception/GlobalExceptionHandlerTest.java)、 +[领域异常](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/LocalizedDomainException.java)。 + +## 场景:新增失败分类或修改统一异常处理 + +先定位失败属于领域、MVC 参数绑定、认证过滤器还是对象存储边界,再选现有异常与 handler。 +例如对象存储读取失败应检查 `handleStorageAccess`;不能在下载服务捕获后返回空 ZIP 并声称成功。 + +| 输入/场景 | 当前可观察契约 | 回归锚点(GlobalExceptionHandlerTest) | +|---|---|---| +| `ObjectOptimisticLockingFailureException` | HTTP 409、响应 `code=409`、本地化冲突提示 | `persistenceConflictsReturn409WithoutLeakingDatabaseDetails` | +| `StorageAccessException` | HTTP 503;日志带 requestId 和认证状态,不输出稳定 userId | `handleStorageAccess_shouldLogAuthenticationWithoutStableUserId` | +| MVC 缺参、不可读请求体、类型不匹配 | HTTP 400、ApiResponse `code=400`;不记作未处理内部故障 | `handleMvcBadRequest_shouldReturn400WithoutUnhandledErrorLog` | +| 不支持的 HTTP 方法 | HTTP 405、ApiResponse `code=405`,保留 Allow 响应头 | `handleMethodNotAllowed_shouldReturn405AndAllowHeader` | + +反例:用 `exception.getMessage().contains(...)` 为新业务规则分配状态码;只断言状态而遗漏协议头; +把原始存储异常文本直接发给客户端。正确做法是沿用有类型的异常、现有消息键与响应工厂, +在对应测试中同时断言状态、响应字段及敏感信息边界。 + +验收时执行 [后端测试入口](../testing/backend-testing.md) 对应测试;修改过滤器链或真实 HTTP 协议时追加 Controller/安全测试。 +上表是当前单测覆盖范围,不证明事务回滚、真实存储可用性或所有入口均经过该 handler。 diff --git a/.trellis/spec/backend/index.md b/.trellis/spec/backend/index.md new file mode 100644 index 00000000..a67360b2 --- /dev/null +++ b/.trellis/spec/backend/index.md @@ -0,0 +1,40 @@ +# 后端规范入口 + +适用 Java/Maven 后端及 Python Scanner 本体。先读本页,再按改动选择专题;工程规范正文在本目录,不依赖工程 Skill 自动触发。 +具体业务需求、权限和状态机归正式 docs/OpenSpec;规范整理不授权改变业务行为。 + +## 日常约定 + +- Controller 负责传输边界,应用服务协调流程,领域服务维护业务不变量;复杂展示查询交给 app 查询仓储。 +- 用户身份全链路使用 `String`;不要把技能或命名空间的数值主键类型套到用户身份。 +- 复用现有仓储、响应工厂、本地化异常与请求追踪,不另造同义基础设施。 +- 参数合法性、授权、领域状态、数据库完整性是不同边界,各自维护所负责的检查。 +- 失败、并发、外部副作用与成功路径一起设计;不以加了事务注解证明跨服务原子性。 + +## 按需读取 + +| 触发条件 | 专题 | +|---|---| +| 新类、模块、依赖或读模型 | [模块边界](./directory-structure.md) | +| 仓储、查询、迁移与性能 | [数据库](./database-guidelines.md) | +| HTTP、鉴权上下文、API消费方、生成类型 | [接口契约](./api-contracts.md) | +| 状态变更、重复请求、竞争或外部副作用 | [事务与并发](./transactions-and-concurrency.md) | +| 异常与响应错误 | [错误处理](./error-handling.md) | +| 发布、下载、删除、授权行为变化,或日志、审计、追踪 | [日志](./logging-guidelines.md) | +| 扫描、队列、超时、重试或 Python Scanner 本体 | [扫描集成](./scanning-integration.md) | +| 实现与审查收尾 | [后端质量](./quality-guidelines.md)、[测试入口](../testing/index.md) | + +生命周期修改读 [正式生命周期文档](../../../docs/14-skill-lifecycle.md) 及对应源码、测试; +API、坐标、权限修改读 [API设计](../../../docs/06-api-design.md)、[领域模型](../../../docs/02-domain-model.md)、[认证授权](../../../docs/03-authentication-design.md)。 +发现文档与实现漂移时列出差异,不能按旧表机械改写代码;已知漂移见接口专题。 +版本和模块清单以 [父 POM](../../../server/pom.xml) 为准,不将某次版本快照写成升级授权。 + +## 业务与基础设施路由 + +| 改动场景 | 修改前读取与核对 | +|---|---| +| 搜索条件、索引更新、搜索引擎替换 | [搜索设计](../../../docs/04-search-architecture.md)、[SearchQueryService](../../../server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQueryService.java)、[SearchIndexService](../../../server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchIndexService.java);分别定位查询与索引端口,不将某个引擎的 SDK 泄漏到业务调用方 | +| 对象存储提供方、上传下载行为 | [架构设计](../../../docs/01-system-architecture.md)、[ObjectStorageService](../../../server/skillhub-storage/src/main/java/com/iflytek/skillhub/storage/ObjectStorageService.java)、[错误处理](./error-handling.md);先核对端口方法、现有实现和调用方,再判断是配置变化还是接口变化 | +| 发布、审核、下载等跨服务流程 | [业务流程](../../../docs/05-business-flows.md)、[事务与并发](./transactions-and-concurrency.md)、[日志与审计](./logging-guidelines.md);列出实际调用链的数据库写入、外部副作用与失败返回 | + +上述是读取触发条件,不表示现有提供方已完成互换验证。涉及替换时按受影响行为验证,不凭“兼容协议”跳过验证。 diff --git a/.trellis/spec/backend/logging-guidelines.md b/.trellis/spec/backend/logging-guidelines.md new file mode 100644 index 00000000..a016faa0 --- /dev/null +++ b/.trellis/spec/backend/logging-guidelines.md @@ -0,0 +1,35 @@ +# 日志、请求追踪与审计 + +适用新增日志、异常日志、异步任务与审计字段。 + +- 使用 SLF4J 参数化日志和既有上下文,记录能定位操作的必要字段。业务事件通常 INFO、调试 DEBUG;预期拒绝不必 ERROR,真正故障保留必要原因。 +- 沿用 RequestIdAccessor/RequestIdFilter 的校验、作用域与清理机制;不要直接信任任意请求头,也不要让线程复用残留上一请求上下文。 +- token、密码、Cookie、OAuth secret、私钥和原始敏感请求体不进入日志;查询串、异常文本、对象 toString 同样可能泄露。 +- 复用适用的脱敏工具并测试新增敏感字段;现有过滤器只覆盖已知字段,存在过滤器不证明所有输入已安全。 +- 区分故障排查日志与业务审计。审计沿既有 AuditLogService 传递可信操作者和必要目标,不能靠普通 INFO 日志替代正式审计。 +- 循环、批处理和重试避免逐条噪声;保留任务标识、失败阶段与重试状态,不记录完整包内容或凭证。 + +修改追踪验证无效 ID、正常响应和异常后的作用域释放;修改敏感字段日志用包含假凭证的测试输入断言其未输出。 + +来源:[请求ID实现](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/RequestIdFilter.java)、 +[请求ID测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/filter/RequestIdFilterTest.java)、 +[请求日志实现](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/RequestLoggingFilter.java)、 +[请求日志测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/filter/RequestLoggingFilterTest.java)。 + +## 场景:关键业务操作的审计随重构丢失 + +修改发布、下载、删除、授权及审核流程时,即使没有改日志代码,也必须检查审计链路。 +先在旧调用链定位事件生产位置,记录 action、目标类型/ID、可信操作者、请求上下文以及记录发生在成功还是失败分支; +新调用链必须保留所需语义。匿名下载等场景应遵循原契约,不能自行伪造用户身份。 +找不到原记录或业务文档与实现不一致时报告缺口,不能宣称已有覆盖,也不借规范整理擅自新增业务政策。 + +具体参考:[PromotionPortalAppServiceTest](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/service/PromotionPortalAppServiceTest.java) +的 `approvePromotion_recordsSelfReviewAuditDetailForSuperAdminSelfApproval`:除了调用成功, +还验证 `PROMOTION_APPROVE`、`PROMOTION_REQUEST`、目标 ID、操作者与 `selfReview` 明细。 +[AuditLogService](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogService.java) +及其 [测试](../../../server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/audit/AuditLogServiceTest.java) +是公共记录入口,不要在各服务自行拼接另一套审计持久化方式。 + +反例:重构后 HTTP 仍返回 200,因而删除旧审计调用;或用一条 `log.info("deleted")` 替代业务审计。 +验收:对受影响操作补/保留 action、目标、操作者与必要明细断言,检查拒绝/失败分支不会记录虚假的成功事件。 +mock 的 `record` 断言只能证明调用参数;改动审计持久化或事务时还需验证实际提交/回滚结果。 diff --git a/.trellis/spec/backend/quality-guidelines.md b/.trellis/spec/backend/quality-guidelines.md new file mode 100644 index 00000000..e0bf42d6 --- /dev/null +++ b/.trellis/spec/backend/quality-guidelines.md @@ -0,0 +1,23 @@ +# 后端质量与行为验收 + +本页定义检查什么;测试层次、fixture、真实依赖与运行方式见 [后端测试](../testing/backend-testing.md), +交付门槛见 [测试入口](../testing/index.md)。 + +| 改动 | 应观察的结果 | +|---|---| +| 分层与模块 | 新依赖方向符合模块约定,领域不返回 app DTO,查询投影没有挤入编排服务 | +| API/DTO/兼容协议 | 成功与错误结构、空值/分页及消费者预期一致,生成类型没有未解释漂移 | +| 授权/命名空间 | 允许场景成功;匿名、无权限、跨命名空间或伪造身份被正确拒绝且不产生写副作用 | +| 状态机/事务 | 前后状态、版本指针和读模型符合正式契约;失败不留下不允许的中间状态 | +| 并发/重复请求 | 竞争结果与契约一致,无重复副作用或旧状态覆盖;锁定后重新判断有效状态 | +| 仓储/迁移 | 查询、排序、total与权限一致;实际数据库执行及存量升级得到验证 | +| 外部服务/队列 | 失败和超时可观察;重试、终态、确认与清理顺序不会丢失必要恢复信息 | +| 日志/异常 | 请求可关联,敏感信息不输出,错误映射与消息资源匹配 | + +## 项目经验:Maven 多模块旧缓存 + +仅在 app 下执行 `./mvnw -pl skillhub-app clean test` 可能使用本地仓库中陈旧的依赖产物,出现误导性的符号或签名不匹配。 +使用仓库 `make test-backend-app`(含依赖模块)或更广的 `make test-backend`;故障排查先确认实际构建模块,不能通过改业务代码迎合旧产物。 +命令实现以 [Makefile](../../../Makefile) 为准。 + +单测通过、真实依赖集成通过、环境健康与生产验收分别报告;静态阅读测试文件不算运行测试。 diff --git a/.trellis/spec/backend/scanning-integration.md b/.trellis/spec/backend/scanning-integration.md new file mode 100644 index 00000000..0c809b37 --- /dev/null +++ b/.trellis/spec/backend/scanning-integration.md @@ -0,0 +1,59 @@ +# 扫描与队列集成 + +适用 Java 后端与扫描服务、Redis Stream、对象存储之间的协议与失败处理; +部署步骤和配置详情引用 [Scanner文档](../../../scanner/README.md),不复制运行手册或固定重试次数。 + +## 工程边界 + +- 领域扫描服务协调扫描状态;outbox dispatcher 发布任务;consumer 处理投递并调用 scanner 适配器。 +- 任务持久化、投递、业务完成和消息确认是不同步骤。发送成功不能证明扫描完成,超时也不能证明远端没有执行。 +- 保持任务/版本身份、原始时间和重试信息在重投递中可追踪;不能每次重新计时导致恢复窗口无限延长。 +- 区分暂时不可用、可重试失败、终态失败与重复消息;策略依据现有代码和配置,不统一“失败立即确认”或“永远重试”。 +- 包下载、临时文件和扫描目录使用现有路径安全边界;清理只针对本次拥有的文件,不能误删并行扫描仍在使用的目录。 + +## 应保留的行为 + +[consumer](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/stream/ScanTaskConsumer.java) +存在任务锁与 pending 恢复逻辑;锁被占用时保留原投递,避免重复消费抹去唯一持久任务。 +扫描暂不可用和超过恢复窗口采用不同路径。失败已记录但确认失败后的重投递不能重复做业务失败处理。 +[outbox dispatcher](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcher.java) +使用 claim、发送标记和退避;修改需要检查租约、发送与标记间失败以及多实例竞争。 + +## 验证锚点 + +[消费测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerTest.java) +覆盖扫描不可用、锁占用、恢复窗口与确认失败后的重投递; +[路径安全测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerPathSafetyTest.java) +覆盖临时路径边界; +[outbox测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/task/ScanTaskOutboxDispatcherTest.java) +用于检查调度行为。 +这些是回归锚点,不意味着本次已运行或真实 Redis/Scanner 链路已验收。 + +## Python Scanner 本体 + +修改 [skillhub_scanner_app.py](../../../scanner/skillhub_scanner_app.py) 的并发限制、取消、超时、响应脱敏或临时目录时, +先读 [本体测试](../../../scanner/tests/test_skillhub_scanner_app.py),不只运行 Java consumer 测试。 + +| 场景 | 当前行为与可观察结果 | 测试名 | +|---|---|---| +| 扫描容量已满 | 新请求返回 503,`Retry-After: 30`,不启动额外扫描 | `test_excess_scan_is_rejected` | +| 客户端取消,但底层扫描尚未结束 | 容量仍被占用,新请求仍拒绝;原扫描结束后释放容量 | `test_client_disconnect_keeps_slot_until_scan_finishes` | +| 扫描超过硬超时 | 调用重启处理;真实实现记录 critical、关闭日志并退出进程 | `test_hard_timeout_requests_process_restart`(替换重启函数,未真正退出进程) | +| 响应 findings 含敏感文本 | 脱敏但保留原响应契约与正常文本 | `test_scan_findings_redact_credentials_without_changing_response_contract`、`test_safe_scan_findings_are_unchanged` | +| 初始化临时目录 | 仅重建私有运行目录,不清理父目录 | `test_import_recreates_private_runtime_temp_root_without_touching_parent` | + +反例:HTTP 请求一取消就释放并发槽位,但底层线程仍扫描;再次重试会绕过容量限制。 +沿用现有 shield/等待与 finally 释放路径;不要将客户端断开等同于计算已停止。 + +日志使用现有 `logging.getLogger(__name__)` 与参数化消息,新增日志不得包含 API key、上传包全文或未脱敏 findings。 +当前包装层源码未配置 JSON formatter;不能把“结构化 JSON 日志”写成已具备的保证。 +日志格式或容器退出恢复变化还需检查 [监控手册](../../../scanner/docs/monitoring-guide.md) 和运行配置。 + +仓库根目录运行现有 [CI](../../../.github/workflows/pr-scripts.yml) 使用的命令: + +```bash +python3 -m unittest discover -s scanner/tests -p 'test_*.py' +``` + +期望所选用例全部通过。此套测试用替身隔离上游服务,不能证明 Cisco 扫描引擎、真实 HTTP 断连、容器重启或 Redis 链路已验收; +这些边界发生变化时按 [部署规范](../deployment/index.md) 补环境验证。 diff --git a/.trellis/spec/backend/transactions-and-concurrency.md b/.trellis/spec/backend/transactions-and-concurrency.md new file mode 100644 index 00000000..3b692053 --- /dev/null +++ b/.trellis/spec/backend/transactions-and-concurrency.md @@ -0,0 +1,34 @@ +# 事务、并发与外部副作用 + +适用状态迁移、批量写入、重复请求、异步处理及对象存储/消息副作用。 +本页要求显式分析契约,不给所有接口增加幂等键、分布式锁或统一重试。 + +## 修改前确定边界 + +- 列出必须共同提交的数据以及可延迟的副作用,确定事务真正由哪个 Spring 代理调用进入。 +- 明确前置校验到写入之间是否存在竞争;预检查不代替锁定后或条件更新时的状态校验。 +- 对重复提交说明复用结果、拒绝冲突还是允许重复;根据现有业务契约选择机制,不能自行改变产品语义。 +- 定义失败后的可观察状态:回滚哪些数据、已经发出的消息或上传文件由谁补偿、如何发现和恢复。 +- 取消请求/超时只表示一段等待结束,不能推断数据库或外部系统已经撤销操作。 + +## 现有实现模式及适用范围 + +[扫描重试服务](../../../server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SecurityScanRetryAppService.java) +先做标量状态预检查,再锁定版本并重判;有活动扫描时复用响应。这个模式只证明该用例的处理,不意味着全部 API 幂等。 +[并发回归测试](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SecurityScanRetryLockingTest.java) +用两个事务验证等待锁后看见已提交新状态,避免持久化上下文中的预加载旧实体误导决策。 +复用经验时检查 ORM 缓存、锁顺序和超时,不只看有无 `@Transactional`。 + +[版本治理](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java) +将对象删除放在提交后,失败交给补偿;数据库回滚不能自动恢复外部删除。 +[补偿服务](../../../server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillStorageDeletionCompensationService.java) +区分普通清理失败与发布回滚后的独立事务记录。修改时检查实际代理调用与提交时序,不把“记录补偿”视为已可靠持久化的证明。 + +## 验证要求 + +- 竞争测试控制交错顺序并断言最终持久状态;单线程 mock 不证明真实锁语义。 +- 在事务提交、外部调用失败、重投递等关键边界注入失败,验证无丢失/重复副作用及补偿可达。 +- 幂等范围若涉及任务ID、版本或请求键,验证同键与不同键,不以请求次数推定逻辑执行次数。 +- 检查拒绝/回滚时事件、审核记录、指针与对象文件的一致性;正式业务预期引用生命周期文档并核对其漂移。 + +测试设计见 [后端测试](../testing/backend-testing.md),队列细节见 [扫描集成](./scanning-integration.md)。 diff --git a/.trellis/spec/cli/index.md b/.trellis/spec/cli/index.md new file mode 100644 index 00000000..34f23736 --- /dev/null +++ b/.trellis/spec/cli/index.md @@ -0,0 +1,64 @@ +# CLI 工程规范 + +修改 `cli/` 的命令、安装、配置或凭证时读取。CLI 使用自己的 Bun 工具链和 Node 运行产物, +不套用 Web 的 pnpm、React 规则。命令说明见 [CLI README](../../../cli/README.md), +工具和构建入口见 [package.json](../../../cli/package.json)与[Makefile](../../../Makefile)。 + +## 分层与输出契约 + +- 命令负责参数和展示,业务编排复用 services,持久化使用 stores,系统能力使用 platform; + 新行为先查对应模块和测试,不在命令中复制下载、凭证或安装逻辑。 +- 输出走现有 [output.ts](../../../cli/src/shared/output.ts) 与 [CliError](../../../cli/src/shared/errors.ts)。 + 人类格式与 `--json` 是不同消费契约;JSON 模式不能混入提示/进度导致无法解析。 +- 保持各命令 stdout/stderr 和退出码语义。参数解析失败的既有契约为 stderr、退出码 5; + 其他失败从现有错误映射核对,不擅自把所有失败归为 1。 +- 错误区分参数、认证、服务响应、网络和本地文件失败;只输出安全的上下文和可执行下一步, + 不泄露 token、完整凭证或内部堆栈。服务 requestId 存在时沿现有字段传递。 +- 交互判断复用 [tty.ts](../../../cli/src/shared/tty.ts):stdin/stdout 都是 TTY 且非 JSON。 + 无人值守时按命令契约要求显式参数或安全失败,不能偷偷选择破坏性默认值。 + +## 配置、身份与本地状态 + +[registry-service.ts](../../../cli/src/services/registry-service.ts) 定义 registry 优先级: +显式参数 → `SKILLHUB_REGISTRY` → config → 内置默认;token 为显式参数 → `SKILLHUB_TOKEN` → 对应 registry 的存储凭证。 +修改时同步子进程集成测试,不只测 resolver。不同 registry 凭证不能串用;登出只移除目标凭证, +不删除其他 registry、配置或安装记录。配置写入保留不属于本次更新的字段。 + +安装、升级、删除、同步修改需明确源身份、目标目录、已有用户修改和失败后状态。 +复用现有目标锁、inventory 和 archive 能力;并发操作不得丢失另一安装的记录, +中途失败不能伪报完成或遗留与实际文件不符的成功状态。具体 overwrite/force 行为以命令正式契约为准, +不在规范迁移时改变用户数据策略。 + +## 测试与验收 + +CLI 行为或工具链变化运行 `make lint-cli`、`make typecheck-cli`、`make test-cli`、`make build-cli`; +仅帮助显示文案按[测试矩阵](../testing/index.md)执行相关输出测试,文档修改检查来源与链接。 +CI 另做多平台和 Node 产物运行,其实际必需检查不因本地按影响选测而豁免。 +单元测试覆盖规则,子进程测试同时断言退出码、stdout/stderr、目标文件和状态记录。 +复用 [临时环境](../../../cli/test/helpers/temp-env.ts)、[进程 helper](../../../cli/test/helpers/run-cli.ts) +及 fake registry,隔离 home/cwd/凭证/宿主 `SKILLHUB_*` 环境;清理本次资源,测试不调用真实桌面应用或发布服务。 + +回归锚点:[错误输出](../../../cli/test/integration/error-output.test.ts)、 +[身份优先级](../../../cli/test/integration/auth-resolution.test.ts)、 +[多进程并发](../../../cli/test/integration/concurrency.test.ts)。mock registry 的通过仅证明 CLI 消费契约, +不能标为真实服务端验收。公共 API 变化追加[后端接口契约](../backend/api-contracts.md)和[测试入口](../testing/index.md)。 + +### 例:修改参数错误或身份配置解析 + +| 改动 | 现有代表用例与结果 | 容易漏掉的验证 | +|---|---|---| +| 未知命令的 JSON 输出 | `runCli(['foo', '--json'])`:退出码 `5`,stderr 可解析为 `{ok: false, message: 'unknown command "foo" for "skillhub"', exitCode: 5}` | 只匹配错误文字会漏掉 JSON 前混入提示或退出码被改成 `0` | +| token 优先级 | fake registry 仅接受显式参数的假 token,环境变量放另一假值;`whoami` 应成功返回该 registry 的用户 | 只测 resolver,不能证明命令将参数传入了 resolver | +| registry 优先级 | 两个 fake registry 使用不同用户和 token;显式 registry 胜过环境变量,环境变量胜过 config | 不同 registry 返回相同 fixture 会掩盖请求发往错误服务的问题 | + +上表来自链接的集成测试;新增 stdout 纯净性等断言时另行实现,不把表格当成所有检查都已有覆盖。 +在已按 CLI 工具链安装依赖的环境,从仓库根目录执行局部反馈: + +```bash +cd cli +bun run test test/integration/error-output.test.ts test/integration/auth-resolution.test.ts +``` + +`bun run test` 保留 package.json 的 pretest 元数据生成步骤;测试会派生本地 CLI 进程、 +启动本地 fake HTTP registry 并使用临时配置,不应访问真实 registry。 +此命令验证这两种行为,不能代替 CLI 行为改动所需的完整 lint/typecheck/test/build。 diff --git a/.trellis/spec/deployment/index.md b/.trellis/spec/deployment/index.md new file mode 100644 index 00000000..de7270c3 --- /dev/null +++ b/.trellis/spec/deployment/index.md @@ -0,0 +1,117 @@ +# 部署与运行规范 + +Bootstrap 管理员用于首次启动引导,不作为共享交付凭证;交付检查初始化开关、凭证来源与首次初始化后的访问控制, +配置字段及行为核对[认证设计](../../../docs/03-authentication-design.md)和当前启动实现,不复制默认口令。 + +## 适用范围 + +修改容器、Compose、Kubernetes、镜像发布、运行配置、健康检查或运维接入时读取。 +这是工程规则入口,不是执行部署的授权;只读分析不启动服务、不发布镜像。 + +## 通用原则 + +- 区分开发、回归、交付环境,先识别目标、现有服务和数据卷,避免影响无关环境。 +- 配置由环境或现有配置机制注入;示例与真实凭证分离,日志和报告不输出密钥。 +- 发布产物应可追溯到版本或提交;配置、数据迁移和回退能力需要分别检查。 +- 构建成功、配置校验、服务健康、完整业务验收是不同证据,不能相互替代。 +- 运行前读实际脚本与变量来源;不凭文档示例猜测命令参数、默认地址或当前环境状态。 + +## SkillHub 约定 + +- 开发入口是 `make dev-all`;正式单机交付参考 `compose.release.yml` 和部署手册。 + 开发能力与交付能力分开,交付环境不得误开启本地 mock 认证旁路。 +- 发布配置检查入口为 `make validate-release-config`,读取 `.env.release`;缺少配置时报告缺项, + 不为了让检查通过写入真实凭证或输出配置文件内容。校验脚本部分错误可能带出配置值, + 保存和分享输出前先脱敏,不能因命令名含 validate 就认为输出安全。 +- 镜像与 Helm 发布行为查 `.github/workflows/publish-images.yml`、`publish-chart.yml`; + 标签、架构与触发条件以当前工作流为准。发布和部署仍需要任务已有的相应授权。 +- Kubernetes 配置、存储和 Secret 的完整要求读对应手册,不在此复制参数表。 +- 配置变更检查应用绑定、默认值、环境透传和实际生效路径;文件中存在某个键不代表环境已经采用。 +- 数据迁移需说明备份、兼容性和回退限制;回退应用镜像不等于回退数据库。 + +## 条件性深读 + +| 条件 | 来源 | +|---|---| +| Compose、Profile、镜像标签、配置、上线与迁移 | [部署架构与运维](../../../docs/09-deployment.md) | +| 开发或 Staging 回归环境 | [开发工作流](../../../docs/dev-workflow.md)、[Makefile](../../../Makefile) | +| Kubernetes | [用户部署指南](../../../docs/skillhub/guide/kubernetes.md)、[清单说明](../../../deploy/k8s/README.md) | +| 请求关联、日志、Trace 接入 | [可观测性开发者指南](../../../docs/observability-developer-guide.md) | +| 扫描服务运行与失败语义 | [扫描运行指南](../../../docs/security-scanning.md) | +| 真实部署验证与报告 | [测试与质量](../testing/index.md) | + +## 需要核对的边界 + +部署文档对支持的运行方式有收敛描述,而 Makefile 与开发文档仍保留 `make staging`。 +先区分正式交付与回归验证,不据此自行删除 Staging,也不把它宣称为生产部署标准。 + +验证配置改动时检查配置校验、构建和相关 Smoke;操作实际环境前确认授权与影响范围。 +验证报告写明环境、版本、命令及实际观察结果;不能继承其他 worktree 或历史发布的通过结论。 + +## 配置与发布的验收契约 + +配置修改应追踪“声明 → 应用绑定/默认值 → Compose 或 Helm 透传 → 进程实际使用”。 +区分构建时与运行时变量;仅修改部署清单或模板不能证明浏览器和服务已经读取新值。 +缺省、空值、非法值、旧配置兼容按本次变化验证,敏感值只证明是否存在/生效,不打印内容。 + +发布前为受影响范围回答以下问题,并将操作步骤留在对应运行手册: + +| 变化 | 需要明确的兼容与失败行为 | 验收证据 | +|---|---|---| +| 应用或镜像 | 新旧 API/CLI/前端能否共存;镜像与提交对应关系 | 实际运行版本与关键请求结果 | +| 配置或 Secret | 默认值是否改变、是否需重启、旧配置是否仍可用 | 绑定和透传检查,启动后有效行为 | +| 数据迁移 | 旧数据如何升级、失败阻止启动还是可恢复、旧应用是否读得懂新 schema | 隔离库升级测试、迁移记录与升级后业务断言 | +| 异步任务/外部依赖 | 切换期间任务如何继续、重复执行如何处理、超时失败如何观察 | 受影响队列/任务状态和副作用,不只健康接口 | + +Flyway 是 schema 变更入口,应用容器启动会执行迁移;具体规则见 +[数据库规范](../backend/database-guidelines.md)和[部署手册](../../../docs/09-deployment.md)。 +因此首次启动新应用本身可能修改数据库,不能把“先启动看看”当作无副作用预检。 +涉及不兼容字段或数据转换时,先明确应用/迁移顺序和兼容窗口;不能只写“回退镜像即可”。 +备份应有对应恢复方式和可验证性;恢复数据、补偿副作用、回退应用是不同操作,执行需在已有授权范围内。 + +## 分层验收与收尾 + +### 例:修改 Web 子路径配置 + +本例说明如何落实“配置声明到实际使用”的检查;不是新的部署默认值。 +涉及 `SKILLHUB_WEB_BASE_PATH`、同源 `SKILLHUB_WEB_API_BASE_URL` 或公开地址时,联读 +[校验脚本](../../../scripts/validate-release-config.sh)、[Compose 透传](../../../compose.release.yml)、 +[容器入口](../../../web/docker-entrypoint.d/20-base-path.sh)与[Nginx 模板](../../../web/nginx.conf.template)。 + +| 代表输入 | 应检查的结果 | +|---|---| +| Web base `/skillhub/`,同源 API base `/skillhub`,公开地址 `https://skillhub.example.com/skillhub` | 路径一致;生成带前缀的静态资源与 Nginx 路由 | +| Web base `/skillhub/`,同源 API base `/other` | 配置校验及运行入口拒绝,不静默使用另一 API 路径 | +| Web base `/api/` 或 `/foo/../bar/` | 拒绝保留前缀或点路径,避免遮蔽服务器路由或路径归一化后失配 | +| 根路径 `/` | 不生成重复子路径 location;静态资源保持根路径 | + +不要只改 Compose 加一个变量就判定完成;也不能仅校验字符串末尾相同就接受公开地址的路径。 +例如 `/other/skillhub` 与 `/skillhub` 不等价。独立 API 主机的绝对 URL 是允许的例外,不能套同源路径相等规则。 + +从仓库根目录运行已有代表检查: + +```bash +sh scripts/tests/web-base-path-routing-test.sh +bash scripts/tests/validate-release-config-test.sh +``` + +前者使用临时 HTML 和配置文件检查容器入口生成结果,不启动 Nginx;后者使用假配置, +依赖 Docker Compose 的 `config` 渲染但不启动服务。它们均不是读取真实 `.env.release` 的交付校验, +也不能证明浏览器、OAuth 回调或反向代理实际可用。 +需要容器 HTTP 验证时再读取[子路径 Smoke](../../../scripts/tests/web-base-path-nginx-smoke-test.sh), +核对它的镜像、容器创建与清理范围后执行。 + +### 证据层次 + +1. 静态配置和构建:证明配置结构、产物可构建,未证明部署可用。 +2. 启动与健康:证明服务就绪、依赖连通,未证明权限和业务闭环。 +3. 真实业务 Smoke:检查受影响入口、读写、权限拒绝与结果状态;结合 + [E2E/Smoke 规范](../testing/e2e.md)隔离账号及数据。 +4. 记录环境、版本、执行命令、观察结果、未验内容;清理只针对本次创建的资源。 + +已有运行环境与端口必须先识别。不要按旧工作流建议直接 kill 占用端口的进程;先确认所有者和影响。 +`make clean`、`make dev-all-reset`、`make db-reset` 含卷或本地数据清理,不能用作常规失败重试。 +`make staging-down` 只清理对应栈且不代表获得删除共享资源的授权。 + +来源:[发布配置校验](../../../scripts/validate-release-config.sh)、 +[Compose 发布清单](../../../compose.release.yml)、[Makefile](../../../Makefile)。 diff --git a/.trellis/spec/frontend/component-catalog.md b/.trellis/spec/frontend/component-catalog.md new file mode 100644 index 00000000..ed785b11 --- /dev/null +++ b/.trellis/spec/frontend/component-catalog.md @@ -0,0 +1,49 @@ +# 组件目录与选型 + +这份共享目录维护盘点方法、维护决策和候选分类;它不声明某个分支已安装或已通过验证。 +当前检出的具体组件表、官方差异与执行结果记录到当前任务,避免内部或旧分支快照随模板传播。 +开始组件工作前重新核对目录、组件 API、调用方和测试。 + +## 当前检出盘点格式 + +每个组件至少记录: + +| 字段 | 要求 | +|---|---| +| 检出身份 | origin、工作树、分支、基线、核对日期;只保留在任务记录 | +| 本地路径与用途 | 实际文件、导入方式、基础/业务组合归属 | +| 公开 API | 源码中实际导出、原生属性、ref、变体、受控契约;不得编造官方属性 | +| 底层与调用方 | 原生 HTML/Radix/自有实现、代表调用方与相关测试 | +| 官方参考 | 当前适配来源及版本;区别参考和来源证明 | +| 差异与结论 | 保留/调整/增加变体/按需补缺,写理由与必须保留的行为 | +| 实现与验证 | 已存在、待适配、已检查、未验证;区分源码、工程检查与浏览器证据 | + +常见盘点对象包括 Button、Input、Textarea、Label、Card、Table、Select、Dropdown Menu、 +Dialog、Tabs;数量以当前文件为准,不能从这份示例清单推断实际文件存在。 +Button 变体与 loading/asChild、Dialog 的底层、Tabs 的交互尤须读取源码。 + +业务无关组合按实际存在情况调查页头、空态、分页、确认、骨架、复制与通知; +已有能力满足需求时保留,不因官方同名组件存在就另建一套。 + +## 官方参考与兼容性 + +官方组件目录:。 +配置:。 +Tailwind 迁移说明:。 +具体组件文档与注册表在使用时核对;示例链接不是当次 URL 存活或兼容性验证结果。 +保留当前合理定制,逐项检查生成目录、导入、依赖和动画插件;不直接复制不兼容的新栈示例。 + +## 按需候选 + +| 场景 | 可调查项 | 边界 | +|---|---|---| +| 表单组合 | Field、Input Group、Native Select | 先检查现有 Label/Input 组合和当前栈兼容性 | +| 输入选择 | Combobox、Checkbox、Radio Group、Switch | 先查原生控件或已有业务能力,再判断是否需要新原语 | +| 展示 | Badge、Avatar、Separator、Accordion、Collapsible | 不把候选写成可直接 import 的已安装组件 | +| 数据表 | Data Table | 组合方案;优先现有 Table/分页,排序筛选按业务需求定义 | +| 导航 | Breadcrumb、Navigation Menu、Sidebar、Command | 沿用 router 与已有导航,缺口成立才增加 | +| 弹层 | Alert Dialog、Popover、Tooltip、Sheet、Drawer | 需要实际验证焦点、Portal 与底层兼容性 | +| 反馈 | Alert、Spinner、Progress | 现有文字/图标/通知能满足时不另建体系 | +| 专门需求 | Calendar、Date Picker、Chart、Resizable、Scroll Area | 真实需求成立再查依赖,不默认安装大型工具链 | + +清单是调查入口,不代表对应版本、文档、注册表或依赖已经在当前任务验证。 diff --git a/.trellis/spec/frontend/component-guidelines.md b/.trellis/spec/frontend/component-guidelines.md new file mode 100644 index 00000000..1a70c3aa --- /dev/null +++ b/.trellis/spec/frontend/component-guidelines.md @@ -0,0 +1,48 @@ +# 组件复用与定制 + +先读[视觉基础](./visual-foundations.md),再按需读[组件目录](./component-catalog.md)。 +读取当前检出适用的 AGENTS/CLAUDE、[前端入口](./index.md)、架构文档、源码与测试;不存在的文件 +不假称已读。不同检出的工程依赖或 API 有差异时先核对,不继承其他工作树的实现状态。 + +采用 shadcn/ui 源码体系,保留 React 19、Tailwind 3 与既有 Radix。 +“不使用 shadcn 运行时库”不等于禁止参考官方源码;不安装另一套整包 UI 体系。 +官方 shadcn Skill 提供 API/CLI 用法,项目规范提供设计与兼容边界。 + +## 选择与放置 + +选择顺序:已有 shared/ui、shared/components、业务组件 → 兼容官方源码 → +布局组合 → 必要的共享变体或实现调整。图标复用 Lucide;合并用既有 cn;文案用 i18n。 + +| 需求 | 放置位置 | +|---|---| +| 单处宽度、排列、响应式 | 调用处 className | +| 多处复用的语义外观或尺寸 | 检查既有变体,再扩展基础组件变体 | +| 所有控件的默认样式、焦点、键盘 | shared/ui 实现,核对直接/间接调用方 | +| 全站字体、主题、密度 | CSS 变量与 Tailwind 映射 | +| 安装面板、技能卡片、业务权限 | 原业务目录,不塞进基础组件 | + +例:单页面 `className="w-full sm:w-auto"` 合理。 +例:每页重写整套按钮颜色和焦点方案会破坏一致性,应使用已有变体。 +不要为文档树把已有效运行的业务组件搬到另一个 FSD 层。 + +## 官方组件适配 + +在当前 web 工程核对 CLI 实际版本和帮助,再执行 info 与代表组件 dry-run。 +安装版本、命令、路径、依赖预告和执行结果写入任务记录,不固定成模板的永久版本。 +核对 components/ui/utils/lib/hooks 与 TypeScript、Vite 别名,不能产生第二套 ui 目录。 +兼容 Tailwind 3 的来源逐项核对;修改 components.json style 不等于迁移已有组件。 +禁止无差别 init、批量 add 或覆盖定制源码。候选组件不意味着必须加依赖。 + +## 组件契约 + +保留原生属性、事件、ref、受控/非受控状态、className/style、标签、aria、表单语义、 +Portal、滚动与焦点。先检查实际 API,不能把官方或其他工作树的属性当作本地已提供。 +自有 Dialog/Tabs 即使外观相似,也不能宣称完整等价于 Radix。 + +Tabs 的 Tab 入口、方向键、Home/End、禁用项处理属于组件基础可用性。 +存在 enabled 项而无有效选中项时,应保留 enabled 键盘入口;入口恢复不能自动选择或 +触发业务回调。全部禁用时没有可操作入口。受控值、动态移除/禁用都需行为验证。 +这不要求增加全站快捷键功能。 + +弹层验证 Escape、Tab 范围、焦点返回和嵌套 Portal。表单验证标签、错误关联与提交语义。 +详细检查与证据规则见[验收规范](./quality-guidelines.md)。 diff --git a/.trellis/spec/frontend/directory-structure.md b/.trellis/spec/frontend/directory-structure.md new file mode 100644 index 00000000..73075886 --- /dev/null +++ b/.trellis/spec/frontend/directory-structure.md @@ -0,0 +1,28 @@ +# 前端目录与依赖归属 + +适用于新增文件、拆分组件、移动逻辑和导入调整。具体架构背景见[前端架构](../../../docs/08-frontend-architecture.md)。 + +| 位置 | 职责 | +|---|---| +| `web/src/app/` | 应用入口、路由、Provider 与整体布局 | +| `web/src/pages/` | 路由级页面及页面组合 | +| `web/src/features/` | 业务功能与该功能的交互、Hook | +| `web/src/entities/` | 领域对象展示 | +| `web/src/shared/` | 跨功能基础组件、工具与共享能力 | +| `web/src/api/` | 请求边界、API 类型适配;generated 为生成结果 | +| `web/src/i18n/` | 国际化配置与翻译资源 | + +选择能承担语义的最低合适层,而不是把所有逻辑下沉到 shared。页面专属权限、导航、发布流程 +留在业务目录;组件共享取决于职责与变化原因,不取决于出现次数。已有共享业务 Hook 的分布 +不代表新增功能都应继续集中在一个文件;不为了符合示意树搬动有效代码。 + +移动时核对直接导入、别名、barrel/re-export、字符串路径、测试和调用方。先复用 +[组件约定](./component-guidelines.md)中的基础能力;业务规则不得为迁就通用组件 API 被改写。 +技术栈和具体版本读取[依赖声明](../../../web/package.json),不在规范里维护易过期功能或依赖清单。 + +## 验证与锚点 + +检查路由能定位页面、原导入仍解析、共享模块没有新增不必要的页面依赖;检查方法见 +[前端测试](../testing/frontend-testing.md)。示例:[应用装配](../../../web/src/app/providers.tsx)、 +[业务 Hook 兼容导出](../../../web/src/features/skill/use-search-skills.ts)及其 +[导出测试](../../../web/src/features/skill/use-search-skills.test.ts)。导出测试只证明路径有效,不证明请求行为。 diff --git a/.trellis/spec/frontend/hook-guidelines.md b/.trellis/spec/frontend/hook-guidelines.md new file mode 100644 index 00000000..b6d25395 --- /dev/null +++ b/.trellis/spec/frontend/hook-guidelines.md @@ -0,0 +1,68 @@ +# 请求 Hook、缓存与并发行为 + +适用于请求、Mutation、缓存键、失效、取消和重试修改。服务器状态使用 TanStack Query, +不在 `useEffect` 中另建取数与缓存链。Effect 可用于确需的外部订阅等副作用,但要清理资源; +命令式上传、下载等操作复用现有 API 边界,不为形式一致改写其业务语义。 + +## 工程约定 + +- 复用 `web/src/api/client.ts` 的类型化请求边界;不要绕过既有 CSRF、基础路径、语言头与错误处理。 + 生成类型和现存 wrapper 的边界见[类型安全](./type-safety.md)。 +- 业务 Hook 放所属功能;跨功能稳定能力才放 shared。返回值明确区分加载、失败、数据和操作状态。 +- 缓存键纳入影响响应的筛选、排序、分页、资源和必要的身份/语言维度。复用已有键工厂, + 让读取、写入和失效针对同一身份;不无条件清空全站缓存。 +- Mutation 成功后按影响更新详情、列表、计数或权限缓存;检查当前筛选/分页是否仍成立。 + 错误处理遵守当前局部与全局处理边界,避免重复弹提示。 + +## 具体例子:列表排序与联动刷新 + +[usePromotionList](../../../web/src/features/promotion/use-promotion-list.ts)先把默认分页、状态与排序 +归一化为 `normalizedParams`,同一个对象用于 `['promotions', normalizedParams]` 和 API 调用。 +因此 `sortDirection: 'ASC'` 与 `'DESC'` 的结果不会共用一个缓存身份。 + +```ts +// 正例:请求条件与缓存身份使用同一份归一化参数 +queryKey: ['promotions', normalizedParams], +queryFn: () => promotionApi.list(normalizedParams), + +// 反例:请求已改变排序,键却不区分,可能复用另一排序的结果 +queryKey: ['promotions'], +queryFn: () => promotionApi.list(normalizedParams), +``` + +同文件的审批/拒绝成功回调同时失效 `['promotions']` 和 `['governance']`,因为治理面板也消费 +该结果。添加新的消费者时先定位其 query key;只刷新按钮所在列表可能让另一个页面保留旧状态。 +这不是要求所有 mutation 都刷新这两个前缀,而是要求写出“受影响消费者 → 缓存键”的实际对应关系。 + +验证排序变更时分别断言 API 参数含 ASC/DESC、两个 query key 不相同;验证 mutation 变更时 +断言实际受影响的缓存失效,不能只断言 mutation 请求成功。现有 +[列表测试](../../../web/src/features/promotion/use-promotion-list.test.ts)覆盖前三者中的参数/键, +不覆盖 mutation 刷新或真实 QueryClient 竞态,按此次修改补足对应边界。 + +## 接口与行为契约 + +1. 快速切换 A→B 后,A 的迟到结果不能作为 B 显示或写入 B 的缓存。正确 query key 之外, + 核对手写回调、额外本地状态和同资源并发 mutation;最新可接受结果由业务语义决定。 +2. 支持取消的请求应将查询 signal 传到请求边界并清理监听/计时器。区分主动取消、超时和业务失败。 + 隐藏组件或停止等待不等于服务器写入撤销;需要撤销业务时使用正式接口契约。 +3. 保留旧结果必须是明确的过渡体验,不能误标成新条件的最终结果;跨身份或敏感资源边界不沿用旧数据。 +4. 乐观更新仅在失败可恢复且冲突可处理时使用。记录受影响快照,失败恢复,并与后续成功结果协调, + 避免旧请求失败回滚掉新请求成功。复杂写入可以等待成功后刷新,无须默认乐观更新。 +5. 不盲目重试写操作;先确认重复执行的业务语义。读取重试也需区分授权失败、取消和暂时性失败, + 不能把所有错误统一包装成可重试异常。 + +## 现状锚点与待核实差异 + +- [查询实现](../../../web/src/shared/hooks/use-skill-queries.ts)中搜索键带参数,使用 `keepPreviousData`。 + 源码注释记录减少搜索时卸载抖动的目的;不是所有页面都应保留旧结果的依据。 +- [缓存键测试](../../../web/src/shared/hooks/query-keys.test.ts)与 + [查询测试](../../../web/src/shared/hooks/use-skill-queries.test.ts)是可参考锚点,不能据此声称竞态已全面覆盖。 +- 当前上述查询函数未普遍透传 Query signal;[请求客户端](../../../web/src/api/client.ts)虽支持 signal/timeout, + 仍将 `AbortError` 转成超时错误。新增取消能力需同时核查这一映射及调用方,本文不声称取消闭环已实现。 +- [Provider](../../../web/src/app/providers.tsx)集中默认重试和全局错误处理;端点例外应说明原因,不能复制另一套默认策略。 + +## 验证 + +针对变更覆盖逆序响应、切换后旧请求完成、Mutation 失败恢复、身份隔离、受影响缓存刷新; +涉及取消则断言主动取消与超时的表现。用可控 Promise/网络 fixture 触发,不能用 re-export +存在性或查询配置字符串代替行为证明。测试方法见[前端测试](../testing/frontend-testing.md)。 diff --git a/.trellis/spec/frontend/index.md b/.trellis/spec/frontend/index.md new file mode 100644 index 00000000..ef8a07e1 --- /dev/null +++ b/.trellis/spec/frontend/index.md @@ -0,0 +1,51 @@ +# 前端规范入口 + +适用于 SkillHub 的页面、组件、样式、状态、Hook 和 API 消费代码。先读本页,再按任务 +读取下表中的专题;不要求纯类型或数据逻辑修改加载全部视觉规范,也不要求纯前端任务 +读取全部后端规范。仓库 AGENTS.md 仍适用;CLAUDE.md 仅在当前检出存在时读取。 + +## 通用原则 + +- 先检查已有组件、调用方和测试,再决定复用或扩展;业务逻辑不因“复用”下沉到通用组件。 +- 服务端状态与临时交互状态分开,避免重复缓存和相互同步;状态作用域尽量贴近使用处。 +- 保留键盘、焦点、表单、受控状态和错误反馈契约;加载、空态、错误与无权限分别表达。 +- 类型来自真实接口契约;设计基线、当前实现和验证证据分别记录,不把文档当作能力证明。 + +## SkillHub 约定 + +- 使用现有 React/TypeScript、Vite、Tailwind 工程;版本以当前依赖声明为准。 +- 按 FSD 放置代码:`web/src/pages/` 为页面,`features/` 为业务功能,`entities/` 为领域展示, + `shared/` 为可复用基础能力;不为对齐目录示例搬动已有效工作的业务代码。 +- TanStack Query 管理服务端请求、缓存和失效,不用 `useEffect` 取数;Zustand 管理需要共享 + 的本地/UI 状态,组件内部短暂交互可用组件状态,不把服务端缓存复制进 UI store。 +- API 复用既有客户端(含 `openapi-fetch` 与类型化 wrapper)及 `web/src/api/generated/schema.d.ts`;不使用 `any`。 + 后端契约变化后通过 `make generate-api` 更新生成类型,不手改生成结果。 +- 复用现有 Radix 与 shadcn 源码体系、`cn()`、组件变体和 Tailwind;“不使用 shadcn + 运行时库”不等于禁止参考其源码,不另建整套 UI 体系。 +- 用户可见文案使用 i18next/react-i18next,翻译资源在 `web/src/i18n/`。 +- 视觉规则见专题;字号、密度、组件 API 必须核对当前实现,个人预览例外不自动升级为全站默认。 + +## 按需深读 + +| 修改触发条件 | 读取内容 | +|---|---| +| 新增文件或调整层次 | [目录结构](./directory-structure.md) | +| 修改视觉、布局或可见 UI | [视觉基础](./visual-foundations.md)、[组件规则](./component-guidelines.md) | +| 动效设计、实现或评审 | [交互动效规范入口](./motion-guidelines.md)及其链接的正文;按入口说明为实现/检查 Agent 准备上下文 | +| 选型、新增或盘点组件 | [组件目录](./component-catalog.md);候选不代表已安装 | +| 新增页面或改变页面组合 | [页面模式](./page-patterns.md)及其引用的视觉与组件专题 | +| 修改请求 Hook 或缓存失效 | [Hook 规则](./hook-guidelines.md) | +| 修改状态作用域或 store | [状态管理](./state-management.md) | +| 修改 API 消费、类型或生成 schema | [类型安全](./type-safety.md) | +| 修改架构、路由或共享模式 | [前端架构](../../../docs/08-frontend-architecture.md) | +| 涉及版本状态、权限或公开分发 | [生命周期契约](../../../docs/14-skill-lifecycle.md),并核对相关源码与测试 | +| 新增日志、错误诊断或排查凭证泄漏 | [前端验收:日志与诊断](./quality-guidelines.md#日志与诊断) | +| 组件交互、视觉与页面验收 | [前端验收](./quality-guidelines.md) | + +## 验证与补充参考 + +按改动范围读取[测试与验证入口](../testing/index.md),选择最小相关检查;涉及共享组件、 +路由或用户流程时按影响扩大。可见变化在环境允许时用浏览器验证;未运行或环境失败明确记录。 + +本目录持有前端工程规范正文,不依赖旧工程 Skill 自动发现。架构背景与业务契约仍按上表引用。 +遇到文档与源码或其他规则冲突,记录差异,按适用指令优先级处理,不借整理规范默改业务规则。 diff --git a/.trellis/spec/frontend/motion-guidelines.md b/.trellis/spec/frontend/motion-guidelines.md new file mode 100644 index 00000000..5cb62eaa --- /dev/null +++ b/.trellis/spec/frontend/motion-guidelines.md @@ -0,0 +1,21 @@ +# 交互动效规范入口 + +涉及 Web 交互动效设计、实现或评审时,继续读取[Web 交互动效基础规范](./web-interaction-motion.md)。 +该正文随项目分发,是本项目通用动效原则的权威版本,不依赖个人目录或全局 Skill。 +静态页面且不涉及动效判断时无需加载全文。外部来源、固定版本与引用边界见正文第 8 节。 + +## 读取与任务上下文 + +- 主 Agent 从本页进入正文,不以读到链接代替读取规范。 +- 有任务上下文清单时,将本页与 `.trellis/spec/frontend/web-interaction-motion.md` 同时列入 + 实现和检查的适用上下文;不要假定注入脚本会自动展开 Markdown 链接。 +- 没有任务记录时,直接读取本页和正文;无需为加载规范新建任务。 +- 正文缺失或无法读取时报告实际读取范围,不声称已遵循。 + +## 与项目规范的关系 + +- 通用原则、选择依据、模式、中断、无障碍与验证方法以正文为准。 +- 项目默认值与适配边界见[视觉基础](./visual-foundations.md);控件反馈 150ms 是项目基线, + 不推广为全部动效的通用要求。 +- 先核对现有组件与行为,优先复用;官方 recipe 参数不自动覆盖已有实现。 +- 实现与检查均使用正文第 7 节评价方法,分别说明已验证内容与未验证内容。 diff --git a/.trellis/spec/frontend/page-patterns.md b/.trellis/spec/frontend/page-patterns.md new file mode 100644 index 00000000..27dc3fd7 --- /dev/null +++ b/.trellis/spec/frontend/page-patterns.md @@ -0,0 +1,22 @@ +# 页面组合 + +先读[视觉基础](./visual-foundations.md)和[组件目录](./component-catalog.md)。 +这些模式指导组合,不授权全站或特定页面重设计。示例组件在当前检出存在时复用, +不存在时先核对实际能力;不为对齐文档名字迁移业务代码。 + +- 搜索:标题/输入 → 筛选/排序/结果数 → 结果列表 → 分页。输入有可访问标签, + 窄屏工具栏换行。技能卡片、分页、空态和骨架优先复用。元数据按信息优先级展示, + 不全部变成彩色徽标;空结果给清除筛选动作,失败给重试。 +- 详情:标题/来源/版本 → 安装主要操作 → 文档、版本、文件分区。 + 复用安装命令、复制与 Tabs;长名称/命令可换行或内部滚动。 + 安装与草稿权限遵循既有业务规则,UI 基础不能改写权限。 +- 表单:关联的 Label → Input/Select/Textarea → 帮助或错误 → 主次操作。 + 错误用 aria-invalid 与 aria-describedby;提交中防重复并附文字,失败保留输入。 + 权限错误单独说明,不能冒充空结果。 +- 管理列表:页头/主要操作 → 工具栏 → Table → Pagination。 + 局部 compact 适合高信息密度列表,表格内部横滚。行操作不抢占主要任务; + 危险确认显示明确对象名称,保留取消路径并复用已有确认能力。 + +常规页面区块使用有限 gap-6/8、字段 gap-4、操作 gap-2;场景需要可调整。 +空态与加载态分别处理,骨架调用方提供可访问加载说明。 +复用已有标题、分页、复制、确认和骨架,不引入重复组合或包裹一切的空抽象。 diff --git a/.trellis/spec/frontend/quality-guidelines.md b/.trellis/spec/frontend/quality-guidelines.md new file mode 100644 index 00000000..02a49dfa --- /dev/null +++ b/.trellis/spec/frontend/quality-guidelines.md @@ -0,0 +1,69 @@ +# 前端验收 + +本页规定验收行为;测试设计、隔离与执行方法见[前端测试](../testing/frontend-testing.md), +检查选择与交付门槛见[测试入口](../testing/index.md)。先检查相关组件/调用方,再扩大到共享影响范围。 + +## 工程与组件 + +1. 按改动运行 typecheck、lint、相关单测和构建;共享行为扩大检查范围。 +2. 修改 shadcn 配置时实际执行 CLI info 与代表组件 dry-run,检查目标路径、导入和依赖。 + 不能产生 shared/ui/ui 等重复目录,也不能以 CLI 识别成功证明组件来源或交互正确。 +3. 验证 focus、hover、disabled、busy、invalid、selected、open,及空/加载/错/无权限状态。 + 状态有语义和文字,不只靠颜色;表单标签及错误关联可观察。 +4. Tabs 检查方向键、Home/End、禁用项跳过、受控值与动态变更;无有效选中项仍可键盘进入, + 不自动触发选择回调。Select 检查选中与滚动;Dialog 检查 Escape、Tab 范围、焦点返回与嵌套 Portal。 +5. 密度变化检查控件、表头/单元格横向对齐、卡片间距与字号;验证显式样式覆盖实际生效。 + +## 数据与交互行为 + +- 请求/筛选变化:旧响应不覆盖当前条件;加载、刷新、空结果、失败与无权限分别可辨。 +- 会话/资源变化:私有数据和未完成写入不串到另一身份或资源,表单草稿按既定契约保留或重置。 +- Mutation:成功状态、受影响缓存与列表一致;失败不留下伪成功;重复操作与并发失败恢复不覆盖较新结果。 +- 取消/重试:只在此次改动涉及这些能力时验证其语义,不能用 signal 参数存在作为完成证据。 +- 国际化:新增用户可见文字进入翻译资源,检查插值、空/长内容以及中英文布局;错误消息同样可翻译。 + +## 日志与诊断 + +- 需要记录的错误用 `console.error`,弃用提示用 `console.warn`;避免在生产代码遗留 + `console.log` 调试输出。日志不能替代用户可见的失败提示,也不要在客户端和调用方重复记录同一错误。 +- 禁止输出 token、Cookie、Authorization、密码及包含这些字段的完整请求/响应对象。 + `console.error(error)` 也不是天然安全:先检查异常 message、cause 和附带上下文是否包含敏感数据。 +- 需要定位请求失败时只选择已确认安全的字段,如操作名、HTTP 状态码;不要直接打印登录请求体、 + token 创建结果或完整 URL 查询参数。示例:`console.error('Token creation failed', { status: 500 })`; + 反例:`console.error('Token creation failed', response)`,后者可能带新生成的 token。 +- 日志修改的验收:检查所有成功/失败分支的实际参数;涉及脱敏逻辑时用明显的假凭证标记驱动错误分支, + spy 日志输出并断言该标记未出现。不能用真实凭证做 fixture。 + +当前 [ESLint 配置](../../../web/.eslintrc.cjs)没有 `no-console` 规则;lint 通过不代表完成上述检查。 +日志级别与字段限制需在变更 diff 中审查,本页不宣称已有自动脱敏或前端日志平台。 + +## 动效验收 + +涉及动效变化时,读取[交互动效规范入口](./motion-guidelines.md)及其正文, +按正文第 7 节区分设计评审、功能验证、性能测量和真实用户研究。 +验收范围与例外以正文为准,不以参数一致、截图或媒体查询存在代替验证。 + +## 视觉与页面 + +选择并记录代表性桌面/移动视口,验证明/暗 × regular/compact。 +检查页面无横向溢出、长中英文换行、字体加载、字重、对齐、控件层次和减少动态偏好。 +共享字体/主题/组件变化检查代表登录、搜索、技能详情和管理页,实际页面按检出选择。 +固定 API 响应可作 UI 回归,不代表真实后端或生产验收。 + +个人预览保持紧凑首屏;额外字体层级、间距、圆角、阴影、状态及组合样例可折叠, +验收时展开检查,不因首屏精简丢失覆盖。预览用当前工程组件,局部适配与共享默认有明确区分。 +预览路径、端口、版本和启动命令属于当前任务的运行记录,不是所有工作树的固定能力。 + +## 证据与隔离 + +- 每次报告记录仓库/基线/diff、命令、实际运行环境和结果,不继承其他检出的通过记录。 +- 浏览器启动失败、业务断言失败、未运行分别记录;源码字符串断言或截图存在不能替代交互/视觉检查。 +- 截图检查后才作参考;基准标注预览版本、浏览器、视口、主题、密度、固定输入与字体/动画条件。 + 像素差异先定位字体/动画等来源,不靠更新图片掩盖失败。 +- 个人预览/截图/任务日志先验证未跟踪且被忽略;工程 fixture 与个人入口分离。 + 生产构建不可暴露个人入口,截图和本机路径不能混入工程 diff。 +- 官方 Skill 安装、当前会话读取、新会话发现、AGENTS 加载、hook 信任与实际触发分别验证。 + 未信任 hook 不通过修改信任记录或绕过入口伪造通过。 +- 任务的延期项有明确原因与后续命令;延期不是通过,也不自动变成永久豁免。 + +规范正文不包含个人预览、截图或任务运行状态;工程测试与 fixture 按仓库约定维护。 diff --git a/.trellis/spec/frontend/state-management.md b/.trellis/spec/frontend/state-management.md new file mode 100644 index 00000000..8951811b --- /dev/null +++ b/.trellis/spec/frontend/state-management.md @@ -0,0 +1,34 @@ +# 状态归属与会话边界 + +适用于筛选、表单、缓存、跨组件状态、登录登出及命名空间切换。 + +| 状态 | 默认归属 | 边界 | +|---|---|---| +| 远端数据、缓存、失效 | TanStack Query | 不另复制一份到 Zustand 并靠 Effect 同步 | +| 可分享、刷新后应恢复的筛选/分页 | 路由 URL | 解析、默认值和查询参数一致;检查前进/后退 | +| 编辑中的表单、弹层开关 | 组件或所属功能状态 | 草稿有独立生命周期;不要被后台刷新覆盖 | +| 多组件共享的本地/UI状态 | 现有 Zustand 或所在功能共享状态 | 明确所有者、重置时机;不为单组件临时值建全局 store | +| 可由上述数据推导的值 | 计算结果 | 避免维护第二份可漂移状态 | + +已有页面未使用 URL 的状态,不在纯文档迁移时改变产品行为;新增可分享状态先确定路由契约。 +服务端数据被复制为用户可编辑草稿时,明确初始化、保存、放弃及远端变更冲突的处理。 + +## 身份、权限与资源切换契约 + +- 用户私有缓存应按身份分区,或在会话变化时清理;选择方案时涵盖在途请求和切换后重新订阅。 + 不允许用户 B 看见用户 A 的私有结果。客户端角色判断用于展示,不能代替服务端授权。 +- 命名空间、资源、语言影响响应时,纳入缓存身份或有等价失效机制。资源切换需清理旧选择、草稿、 + 乐观状态;是否保留草稿按产品契约判断,不静默丢弃用户输入。 +- 禁用查询只控制取数,不自动证明缓存已清除;旧数据保留只适用于允许沿用的上下文,不能跨越身份边界。 +- Mutation 成功后更新或失效相关数据,失败后不能显示未成功的服务端状态。具体见[Hook 契约](./hook-guidelines.md)。 + +## 已有实践与证明范围 + +[通知会话工具](../../../web/src/features/notification/notification-session.ts)按用户分区通知键,并清理一组 +会话相关查询;[测试](../../../web/src/features/notification/notification-session.test.ts)验证分区及保留公共搜索缓存。 +[会话建立 Hook](../../../web/src/features/auth/use-session-bootstrap.ts)先调用清理再写入当前用户。 +这些锚点不证明所有接口均已覆盖身份切换:新增私有查询要核对它是否进入这条机制。 + +[语言缓存键](../../../web/src/shared/hooks/query-keys.ts)展示语言维度;不要把一个键工厂推广为所有响应都依赖语言。 +验证应覆盖相关的账号切换、资源快速切换、浏览器前进/后退、未保存表单与后台刷新,方法见 +[前端测试](../testing/frontend-testing.md)。 diff --git a/.trellis/spec/frontend/type-safety.md b/.trellis/spec/frontend/type-safety.md new file mode 100644 index 00000000..0af8d55a --- /dev/null +++ b/.trellis/spec/frontend/type-safety.md @@ -0,0 +1,54 @@ +# 类型安全与 API 边界 + +适用于 API 消费、模型、生成 schema 和类型断言修改。 + +- 保持 TypeScript strict;不新增 `any` 逃避契约。未知外部输入用 `unknown` 并在边界收窄, + TypeScript 泛型或断言不等于运行时校验。 +- API 形状优先来自 `web/src/api/generated/schema.d.ts`,通过既有 API 模块引用; + 展示模型、表单草稿和必要适配可以独立定义,但不能再维护一份会漂移的服务端协议。 +- 用户标识使用字符串;不要因为样本是数字文本便转成 number。资源 ID 的类型依据各自协议,不混为一谈。 +- 不以 `as`、非空断言或手写 Required 掩盖服务端可选字段。适配层强化约束时说明实际保证, + 覆盖缺失/null/未知枚举的适用行为;不能把假设伪装为 schema 事实。 +- 后端 OpenAPI 契约变化后通过 `make generate-api` 生成类型,生成文件随契约修改交付;不手改生成结果。 + 命令实现以 [Makefile](../../../Makefile) 为准,检查前确认所用服务来自本次代码。 + +## 改接口字段时怎么落地 + +例如调整当前用户字段,先查看 [API 类型入口](../../../web/src/api/types.ts) 的 `User`, +再追到生成 schema 的 `AuthMeResponse`。当前 `User` 用 `Omit` 后重声明部分字段;这属于已有 +适配边界,不是服务端自动保证所有字段必填的证据。新增必填假设应核对响应及缺失值处理。 + +```ts +// 消费既有类型,不在组件内复制一份 User 协议 +import type { User } from '@/api/types' +type UserLabelProps = Pick + +// 反例:用户身份可能包含非数字字符,转换会破坏身份 +// const userId = Number(user.userId) +// 反例:断言只改变编译器看法,不能验证接口返回 +// const user = response as User +``` + +新增测试数据可参考 +[晋升列表 fixture](../../../web/src/features/promotion/use-promotion-list.test.ts) 的 +`{ ... } satisfies PromotionTask`:缺字段时修复 fixture 或核实契约,不用 `as PromotionTask` 绕过检查。 +`Partial` 可表达真正的局部编辑状态,但不能用来掩盖最终提交协议所需字段。 + +字段变更后的检查顺序:核对后端响应/schema → 更新已有类型或适配 → 检查消费方和 fixture → +运行 `pnpm -C web typecheck` 与相关测试。仅类型变化不需要引入新的运行时校验库; +若此次需求涉及不可信输入,则在对应边界明确验证及失败行为。 + +## 现有边界 + +[客户端](../../../web/src/api/client.ts)同时包含 openapi-fetch 与手写 convenience wrapper, +[API 类型](../../../web/src/api/types.ts)同时包含生成类型映射和手写接口。 +新增消费复用这些入口并优先生成类型;不能宣称现有全部请求已经生成类型覆盖, +也不在规范整理中无范围限制地重写所有 wrapper。 + +[编译配置](../../../web/tsconfig.json)启用 strict; +[客户端测试](../../../web/src/api/client.test.ts)提供请求边界用例。审查时检查响应字段、请求参数、 +可选性、身份类型与消费方一致,必要时补充边界测试。 + +生成一致性检查入口为 `scripts/check-openapi-generated.sh`,该脚本会启动本地依赖且清理时执行 +Compose down,不是纯静态检查;在隔离环境按[测试入口](../testing/index.md)执行。 +不能把类型检查通过等同于真实接口、授权或运行时响应验证通过。 diff --git a/.trellis/spec/frontend/visual-foundations.md b/.trellis/spec/frontend/visual-foundations.md new file mode 100644 index 00000000..0617a5b5 --- /dev/null +++ b/.trellis/spec/frontend/visual-foundations.md @@ -0,0 +1,40 @@ +# SkillHub 视觉基础 + +适用于采用 React 19、Tailwind 3、现有 Radix 与 shadcn 源码体系的 UI 工作。 +这是设计基线,不是所有分支的实现声明。开始修改前核对当前源码、调用方和测试; +已具备能力、差异和验证结果记录在当前任务中。 + +视觉变量的工程来源是 `web/src/index.css`,映射是 `web/tailwind.config.ts`, +组件 API 和实际默认值以当前检出源码为准。规则不替代源码,也不授权全站重设计。 + +| 维度 | 默认与允许变化 | 使用边界 | +|---|---|---| +| 页面、表面、弹层 | background/card/popover 与各自 foreground 成对 | 黑白灰主调;避免页面硬编码白色使暗色失效 | +| 主要操作 | primary 保持近黑/近白;次操作 outline/ghost | 蓝色仅用于有限交互强调,不默认把按钮和卡片全改蓝色 | +| 状态 | info/success/warning/destructive 与 foreground 成对 | 同时提供文字或图标,不能只靠颜色表达状态;先核对变量是否存在 | +| 正文与标题 | 自托管 Inter 400/500/600/700;中文系统 sans 回退 | CSS 与 Tailwind 字体同源;核对实际加载资源,不假设未提供字重可用 | +| 代码与品牌 | 自托管 JetBrains Mono;既有品牌字体限品牌场景 | 品牌字体不作为正文;不增加运行时远程字体请求 | +| 字号 | 页面标题30/36px、区域20/28px、卡片18/28px;正文16/24或14/20px;辅助12/16px | 使用有限尺度;长标题允许换行,场景需要可调整,不以缩字解决拥挤 | +| 字重 | 正文400、表单/次标题500、主要标题600、少量700 | 符合已加载字体字重;中文平台差异需在视觉检查中记录 | +| 间距 | 内部8/12px、字段16px、分组24px、页面区块32px | 使用 gap-2/3/4/6/8;例外按布局场景解释,不增加无人使用的变量 | +| 密度 | regular 控件36px/单元格12px/卡片20px;compact 32px/8px/16px | 设计基线;容器 data-density=regular/compact,使用前确认变量与组件已实现 | +| 尺寸例外 | sm、lg、icon 沿用当前 API;Textarea 不跟随单行控件压缩 | compact 保持默认文字可读;触屏按需求使用常规或更大操作区域 | +| 圆角 | sm=8px、md/lg=10px、xl=14px、2xl=18px;标签可full | 复用 --radius 与既有映射;文档不能声称同值的 md/lg 不同 | +| 边框与阴影 | 边框分组、轻阴影表面、较高层级弹层 | 不给每层嵌套容器加重阴影;检查 className/style 覆盖是否实际生效 | +| 动效 | 控件反馈默认150ms是项目基线;有依据的场景可调整,非必要装饰不新增 | 涉及动效时读取[交互动效规范入口](./motion-guidelines.md)及其正文;按通用规范验证减少动态与交互行为 | +| 响应式 | 页面边距移动约16px/桌面约32px,工具栏换行,表格内部横滚 | 长无空格内容可换行;弹层不超视口;页面不能横向溢出 | +| 主题、焦点与错误 | 明暗状态成对;可见 focus;aria-invalid 关联错误文字 | 不移除焦点提示;错误不能只改变边框颜色 | + +正确:在密度实现已具备的管理表格容器上设置 `data-density="compact"`。 +错误:每个按钮分别写任意高度,并把默认正文缩成10px。 +正确:成功颜色附“已发布”文字。错误:仅用一个绿色圆点表达审核结果。 + +## 密度与局部定制 + +表头与数据单元格横向 padding 应使用同一密度来源。Portal 不自动继承触发器局部 +容器的密度,Select/菜单选项须保留可达尺寸;全局密度与局部紧凑分别验证。 + +单页面宽度、排列、文本域高度、按钮留白和标题字号可以在调用处调整。 +个人预览的局部适配不升级为全站默认;只有多处产品需求成立才按 +[组件规则](./component-guidelines.md) 增加共享变体。 +兼容类与营销页面现有特例不自动成为新页面模板,也不因此批量清理存量。 diff --git a/.trellis/spec/frontend/web-interaction-motion.md b/.trellis/spec/frontend/web-interaction-motion.md new file mode 100644 index 00000000..270da74d --- /dev/null +++ b/.trellis/spec/frontend/web-interaction-motion.md @@ -0,0 +1,299 @@ +# Web 交互动效基础规范 + +版本:1.0 · 资料核对日期:2026-10-15 · 适用范围:跨项目、跨框架的 Web 交互设计与质量评价。 + +本规范回答四个问题:为什么动、怎样选择、不同交互怎样保持一致、如何证明它有帮助。Transitions.dev 是主要设计参考,不是本规范的发布者或认证方。 + +## 1. 阅读约定与参考边界 + +全文采用以下来源标记;标记作用于所在段落或明确标注的表格: + +- **【官方】**:在固定版本的 Transitions.dev 资料中可以直接核对的描述、参数或方法。 +- **【归纳】**:从参考效果中提炼的通用设计原则,不代表官方原文。 +- **【建议】**:本规范为完整性、可用性或验证而补充的建议,可依据实际约束调整。 +- **【外部标准】**:来自 W3C 等资料的要求或指导,保留其适用条件和等级。APG 模式指导与浏览器性能建议不等同于 WCAG 成功准则。 + +“应”表示本规范推荐的基线;例外需要能解释用户收益和验证结果。“可以”表示可选手段。官方参数只是参考起点,不能仅凭数值一致就认定设计正确,也不能据本规范宣称整体符合 WCAG。 + +### 1.1 Transitions 是什么 + +【官方】Transitions.dev 提供交互动效展示、可复用的 CSS recipes、可调变量与面向编码 Agent 的使用指南。官方仓库还包含辅助工具;本规范仅研究交互设计依据,不要求采用这些工具。[官方 README][readme] + +| 资料 | 解决的问题 | 可提炼的通用内容 | 不直接升级为通用规范的内容 | +|---|---|---|---| +| 动效示例 | 看清变化的节奏、方向和视觉关系 | 状态连续性、层级、强调程度 | 展示页布局、配色、品牌风格与演示数据 | +| CSS recipes | 查看一个具体效果如何表达 | 进入/退出关系、动作范围、减少动态处理 | 选择器、DOM 结构、测量方式、具体曲线与数值 | +| Motion tokens | 用有限词汇描述与调整动效 | 按用途选参数、一致性和可解释的例外 | 全量变量集合及其默认值 | +| Agent 指南 | 帮助 Agent 将需求匹配到 recipe | 先识别交互用途,再选效果;合适时选择较低开销方案 | 安装、复制代码及工具执行步骤 | + +【归纳】四者可理解为:示例帮助观察,recipe 提供具体表达,tokens 提供可调语言,指南帮助选择。适合参考的问题包括突兀的状态切换、难以理解的出现位置、缺少操作反馈、过多互不一致的节奏。动效不能弥补不清楚的文案、错误的状态、不合理的信息结构或无法操作的控件。 + +### 1.2 资料中的差异如何处理 + +【官方】本次固定版本的指南仍有“32 个”的描述,但索引列到了 35;README 的部分数量更旧。数量不是本规范的能力或质量依据,以实际读到的条目为准。[指南][guide] + +【官方】`_root.css` 同时包含共享尺度和单个效果的调节变量,并非每个效果变量都引用共享 token。例如共享时长表把 toast close 对应到 350ms,而具体 toast 默认关闭时长为 250ms。不能假定复制共享 token 就能统一所有效果,也不能按“数字接近”机械替换。[tokens][tokens] + +【建议】遇到类似差异,先确定所需交互语义,再比较具体 recipe 与参数;保留有理由的差异。指南中的复制要求属于其使用方式,不成为跨项目设计约束。 + +## 2. 原则:动效应该帮助用户理解什么 + +以下为【归纳】,验证要求为【建议】。 + +| 目的 | 选择依据 | 保持静态更合适的情况 | 可观察的验证 | +|---|---|---|---| +| 状态变化 | 变化不容易被察觉,且旧状态与新状态存在关联 | 高频更新造成持续闪动,或变化本来已经清楚 | 用户能指出现在的状态,不需要等动画结束 | +| 空间关系 | 需要说明来源、去向、展开范围或层级 | 两个对象没有空间关系,移动反而暗示错误联系 | 方向与触发位置、阅读方向和实际层级相符 | +| 操作反馈 | 用户需要知道输入已被接收、正在处理或已完成 | 静态文字、按下态或选中态已足够 | 反馈及时,过程状态和真实结果没有混淆 | +| 注意力层级 | 新信息确实需要被注意 | 内容与当前操作无关,重复提醒打断阅读 | 重要变化可见,次要变化不会争夺注意力 | + +【建议】先用一句话描述“用户需要理解什么”,再选择效果。无法说明收益时,优先使用静态变化。对偶发、重要变化可以适度强调;对高频操作要降低运动幅度和重复成本。品牌表达可以是附加收益,但不能牺牲可操作性或信息完整性。 + +## 3. 选择依据:建立动效设计语言 + +### 3.1 各维度怎样选 + +下表为【归纳/建议】,不是官方数值标准。 + +| 维度 | 作用 | 选择依据与边界 | +|---|---|---| +| 时长 | 决定变化可被追踪的时间 | 高频、局部反馈宜短;复杂关系可留更多观察时间。持续更久不代表更自然,短到难以察觉也可能失去目的 | +| 缓动 | 描述速度如何变化 | 减速有助于落定;连续往复需要衔接;匀速适合表达持续过程。弹性只在额外强调有价值时使用,避免暗示状态反复 | +| 延迟 | 区分意图或组织顺序 | 悬停提示可用短延迟避免误触发;直接操作应先反馈。延迟不能变成等待装饰效果的门槛 | +| 位移 | 表达来源、方向和关联 | 用足以看清关系的距离;大面积、长距离移动成本更高。不要让无关对象沿相同路径制造假关联 | +| 缩放 | 表达层级、出现和轻度强调 | 保持内容可读、位置可追踪;少量缩放通常比强烈弹跳更克制。大区域缩放会放大视觉运动 | +| 透明度 | 表达显隐、旧新内容交接 | 适合关系简单的变化;避免两段文字同时可读造成重影。透明不等于不可聚焦或对辅助技术隐藏 | +| 模糊 | 弱化过渡中的细节 | 可选装饰;会降低瞬时可读性并增加渲染成本。文字密集、低性能或减少动态场景优先省略 | +| 错峰 | 让一组变化体现顺序 | 仅在顺序有意义时使用;总等待随数量增长,不能让末尾内容长期不可用 | + +【建议】同时考虑“多久开始反馈”和“多久结束视觉变化”。用户可以立即得到已接收反馈,视觉过渡稍后完成。顺序播放的总时间包含各段时长、间隔及错峰;并行播放不能简单相加。不要只检查某一个 duration。 + +### 3.2 如何使用语义化 motion tokens + +【官方】指南强调按**用途**匹配 token,而不是按原有数字找最近值。[指南的 Motion tokens 与 refine 说明][guide] + +【建议】语义化意味着能回答“这个值负责什么”:例如“轻反馈”“表面进入”“表面退出”“强调反馈”。这些是本规范的说明性词汇,不是要求新增的变量名。先识别已有语义,同类用途保持一致;只有确实不同的交互需要,才保留独立参数。 + +下面仅摘取少量官方参考,完整定义见 [固定版本 tokens][tokens]: + +| 官方 token | 该版本参考值 | 官方所举用途 | 使用时需要判断 | +|---|---|---|---| +| `--duration-quick` | 150ms | 弹层关闭、文字替换等 | 是否足以理解变化,是否存在额外串行等待 | +| `--duration-fast` | 250ms | 弹层打开、选中指示器移动等 | 频率、范围和内容可用时间 | +| `--duration-stagger` | 40ms | 相邻项目的错峰间隔 | 总数量造成的累计延迟 | +| `--ease-smooth-out` | `cubic-bezier(0.22, 1, 0.36, 1)` | 表面显隐与位置变化等 | 是否符合动作的起止关系 | +| `--distance-micro` | 4px | 文字替换 | 内容是否仍能轻松阅读 | +| `--scale-large` | 0.96 | modal 显隐 | 名称描述其体系内的缩放幅度,不能解释为通用尺寸 | + +【建议】完整的动效约定至少表达:触发条件、变化目的、运动属性、节奏、结束状态、中断策略、减少动态方案。只有一张毫秒表,不能构成设计语言。 + +简短示例:选择变化立即成立;指示器从当前位置移动至新位置;快速改选时转向最新目标;减少动态时直接落位。这里的核心是选择连续性,并不依赖固定时长。 + +## 4. 通用模式:从用户需要理解的变化出发 + +本节每项“官方参考”描述来源能力;“选择、误用、验证”为【归纳/建议】。 + +### 4.1 出现与消失 + +- **用户需要理解**:什么出现了,它与当前内容是什么关系,关闭后回到哪里。 +- **动效如何帮助**:淡入淡出表达显隐,轻缩放表达浮于原内容之上的表面;运动方向应符合真实关系。 +- **官方参考**:[Modal open / close][modal] 展示中心缩放的进入与退出;该版本参考打开 250ms、关闭 150ms。 +- **适用条件**:表面的出现需要建立层级。仅提示小范围状态变化时,静态提示或淡变可能足够,不必引入更重的表面。 +- **不适用与误用**:把所有内容都做成弹出;用相同长时长处理每次关闭;透明退出后仍拦截操作;为了演完动画延迟焦点处理。 +- **验证**:进入、退出、退出中重新打开都能到达正确状态;焦点留在可理解的位置;关闭后不会留下不可见的交互区域。 + +### 4.2 展开与收起 + +- **用户需要理解**:哪些内容属于这个标题,收起隐藏了哪一部分。 +- **动效如何帮助**:让内容区域的变化和展开标记保持对应,减少突然挤动造成的迷失。 +- **官方参考**:[Accordion expand][accordion] 通过区域高度变化与箭头翻转表达展开状态;具体 grid 技巧属于实现细节。 +- **适用条件**:局部内容与触发器有明确归属,尺寸变化可以被稳定呈现。 +- **不适用与误用**:长内容展开产生大面积运动;为控制动画而裁掉内容;视觉收起后隐藏控件仍能获得焦点;箭头与真实状态不一致。 +- **验证**:短长内容、内容动态变化、快速反向操作均能正确落定。若正在隐藏的内容含焦点,焦点去向应有明确安排。区域重排过大时采用静态展开或更轻的提示。 + +### 4.3 切换与替换 + +- **用户需要理解**:哪项被选中,什么内容被替换,是否仍在同一个上下文。 +- **动效如何帮助**:移动选中指示器表达连续选择;轻量交接表达旧内容变成新内容。 +- **官方参考**:[Tabs sliding][tabs-recipe] 展示选中指示器移动;[Text states swap][text-swap] 展示文字退出后新文字进入。前者首屏定位与尺寸调整不播放切换动画。 +- **适用条件**:互斥选项有明确位置关系,或同一位置的状态发生可理解的变化。 +- **不适用与误用**:让指示器移动代替选中语义;文字不断翻动难以阅读;排队播放已经过期的选择;让键盘必须等动画结束。 +- **验证**:快速连续选择后显示最新有效状态;焦点、选中态、内容一致;初次显示没有从错误位置滑入。高频数据更新可以直接替换,保留稳定标签帮助比较。 + +### 4.4 加载与完成 + +- **用户需要理解**:内容尚未准备好,系统仍有响应,何时可以使用结果。 +- **动效如何帮助**:适当占位维持空间预期,轻微活动表达处理中,结果出现后结束活动。 +- **官方参考**:[Skeleton loader and reveal][skeleton] 展示占位与内容的交接;其实际揭示函数与演示重放计时是分开的。 +- **适用条件**:等待具有可感知长度,且能够合理预估内容结构。未知结构可以用简洁的状态提示。 +- **不适用与误用**:为了播放完整循环而延迟已就绪内容;用演示计时代表真实完成;错误发生后仍无限加载;未知进度伪装成准确百分比。 +- **验证**:即时完成、长等待、失败、取消均有清楚状态;内容就绪即可使用;关闭动态后仍能知道正在处理;占位不会制造严重布局跳变。 + +### 4.5 成功与错误反馈 + +- **用户需要理解**:操作是否完成,或哪里需要修正。 +- **动效如何帮助**:有限强调吸引注意,随后保留清楚、可读的结果。 +- **官方参考**:[Success check][success] 展示完成标记;[Error state shake][error] 展示抖动、错误样式和提示,并包含自动恢复的示例。 +- **适用条件**:重要但不频繁的结果需要确认,或用户可能忽略局部错误。抖动和庆祝都是可选手段。 +- **不适用与误用**:每次成功都强烈庆祝;连续输入不断抖动;仅靠颜色或运动表达错误;机械沿用错误提示的自动消失计时。 +- **验证**:结果与真实状态一致;错误未解决时,修正所需信息仍然可获得;减少动态时仍有文字或语义明确的标记。短暂提示只有在不承担唯一必要信息时才适合自行消失。 + +### 4.6 悬停、聚焦和选中 + +- **用户需要理解**:哪些对象可操作、当前焦点在哪里、哪些状态已被选定。 +- **动效如何帮助**:轻量反馈确认当前目标,选中后的稳定外观表达持久状态。 +- **官方参考**:[Tooltip open/close][tooltip] 展示悬停或聚焦提示及短延迟;[Toggle][toggle] 展示开关位置变化;选中位置关系可参考 [Tabs sliding][tabs-recipe]。 +- **适用条件**:提示提供补充说明,状态变化有清楚语义。可选的鼠标装饰不必在键盘下逐帧复刻,但信息和操作能力应等价。 +- **不适用与误用**:把必要信息只放在 hover;把聚焦当作已选中;焦点提示必须等淡入;指针快速经过仍弹出迟到提示;开关弹跳造成状态反复的错觉。 +- **验证**:键盘可用、焦点立即可见、触屏不依赖悬停;取消悬停或转移焦点后不会出现过期提示。适用的额外提示满足可关闭、可悬停、持续显示要求,见第 6 节。 + +## 5. 连续性与可中断性 + +【归纳】连续性不是要求所有动画都完整播放,而是让用户理解当前状态如何变成下一状态。进入与退出应共享对象身份和合理路径,但不要求完全镜像或相同时长。 + +【建议】按以下情境判断,而不是默认禁用操作等待动画结束: + +| 情境 | 推荐行为 | 需要避免 | +|---|---|---| +| 进入中关闭、退出中打开 | 从当前可见状态继续或反向;静态落位也是可接受退化 | 先跳回起点再重播、已关闭的表面突然复现 | +| 同一动作反复触发 | 需要再次确认时给轻反馈;同一目标不必重复完整动画 | 重复输入造成队列越积越长 | +| 快速切换 | 对表达当前状态的动效,以最新有效目标为准 | 旧计时或旧完成通知覆盖新状态 | +| 连续事件有独立意义 | 保留事件信息,可合并视觉呈现 | 为追求“最新”丢掉必须保留的信息 | +| 用户取消 | 表达实际取消结果或待确认状态,停止无意义运动 | 只停止动画却继续显示错误的过程状态 | +| 改变尺寸或内容 | 重新保持目标位置与可读性,必要时直接落位 | 把初始化和重排误演成一次用户操作 | +| 减少动态或关闭动画 | 直接形成正确终态,保留反馈与操作能力 | 依赖必然发生的动画结束事件才能解锁界面 | + +【建议】最低检查序列:打开→立刻关闭→重新打开;连续切换多个目标;处理中取消;内容或尺寸在过渡中改变;动画进行中启用减少动态偏好。每种情况都检查视觉、语义、焦点和可操作性,而不只看最终截图。 + +## 6. 无障碍与性能边界 + +### 6.1 减少动态不是隐藏信息 + +【官方】所读取 recipes 提供 `prefers-reduced-motion` 处理,并要求保留。[指南][guide] + +【建议】减少非必要位移、缩放、弹跳、模糊和循环活动,可直接切到终态,或在适合时采用更轻的透明度变化。减少动态不是把所有状态提示删掉。不能只检查媒体查询存在:还要实际启用偏好,确认等待逻辑、最终可见性、错误提示和焦点仍正确。 + +【外部标准】WCAG 2.2 的 [2.3.3 Animation from Interactions(AAA)][wcag-motion] 要求交互触发的运动动画可被禁用,除非对功能或信息表达必不可少。它不是所有 AA 检查中的同一条强制要求。本规范仍建议将减少动态作为基本体验设计的一部分;必要运动需具体说明为何不可替代。 + +### 6.2 键盘、焦点和状态语义 + +【外部标准】键盘操作、合理焦点顺序与可见焦点分别参见 WCAG [2.1.1][wcag-keyboard]、[2.4.3][wcag-order]、[2.4.7][wcag-focus];状态消息参见 [4.1.3][wcag-status]。视觉动画不能代替这些能力。 + +【建议】焦点不跟随装饰性物体移动,不留在已经隐藏或移除的内容中。选中、展开、错误和处理状态应具有可感知且可由辅助技术识别的表达;不需要为每一帧运动发出播报。 + +【外部标准/建议】Tabs 的方向键、进入方式和自动/手动激活应参考 [WAI-ARIA APG Tabs][apg-tabs],具体选择取决于内容能否及时显示。官方动效 recipe 的点击示例不等于完整 Tabs 行为。无有效选中项时仍应为可用选项保留合理键盘入口;禁用项的焦点策略按控件语义明确,不由滑动效果决定。 + +【外部标准】对于 hover/focus 触发的额外内容,WCAG [1.4.13(AA)][wcag-hover] 在适用条件下要求可关闭、可悬停且持续显示,并规定了例外。不能因为官方提示动画离开触发器就消失,直接认定整套提示行为符合该要求。 + +### 6.3 闪烁、循环与阅读干扰 + +【外部标准】WCAG [2.3.1(A)][wcag-flash] 对每秒闪烁次数及一般/红色闪光阈值有明确要求;不是仅凭动画时长或肉眼“温和”判断。存在高对比快速闪烁时,应按阈值评价。 + +【外部标准】[2.2.2(A)][wcag-pause] 对自动开始、超过五秒、与其他内容并行呈现的移动/闪烁/滚动信息,通常要求暂停、停止或隐藏机制,必要活动有例外;自动更新信息另有控制要求,不套用五秒门槛。 + +【建议】没有触及上述条件,也不意味着持续装饰值得保留。长时间阅读区域优先安静;对处理指示可用静态文字补足,避免为了“还活着”不断占用注意力。 + +### 6.4 渲染成本与收益 + +【外部指导】浏览器性能指南建议尽量使用 transform 和 opacity,检查布局、绘制与合成成本,并谨慎使用 `will-change`。[web.dev 性能指南][performance] + +【建议】这些是优先选择,不是无条件禁令:展开区域可能确实需要改变布局;transform 也不能保证任何场景都流畅。模糊、大面积透明叠加、多个并发动画或大量独立图层可能产生显著成本。是否保留,要依据实际面积、内容数量、设备能力与测量结果。 + +【建议】不要因 recipe 出现 `will-change` 就将其升级为全局默认。关注长期图层和内存成本;先减少无必要属性、缩小运动范围或简化效果,再判断是否需要额外优化。仅在常用设备上看起来顺滑,不足以证明低性能设备可用。 + +## 7. 验证方式:怎样评价一个动效 + +### 7.1 六个判断维度 + +以下为【建议】。每项应有观察结果,不能仅给“流畅、自然”的评价。 + +| 维度 | 要回答的问题 | 合适的证据 | +|---|---|---| +| 明确目的 | 它帮助用户理解哪种变化? | 一句目的说明;与静态方案的对照 | +| 理解变化 | 来源、目标、选中和结果是否清楚? | 观察操作后的解释与误判,而非只问好不好看 | +| 及时且一致 | 是否及时响应,同类变化是否可预期? | 实际反馈延迟、完整序列与快速操作观察 | +| 阅读和操作 | 是否妨碍阅读、点击、键盘或焦点? | 长内容、连续操作、键盘和辅助技术验证 | +| 信息完整 | 减少或关闭动态后是否仍能完成同一操作? | 静态/减少动态模式下的状态与结果检查 | +| 收益与成本 | 视觉收益是否值得渲染和注意力成本? | 有/无该效果的性能对照与用户表现 | + +### 7.2 四类验证各自证明什么 + +| 类别 | 可以证明什么 | 不能单独证明什么 | +|---|---|---| +| 设计评审 | 意图、方向、层级、节奏和一致性是否合理 | 真实可操作性、运行性能、用户收益 | +| 功能验证 | 状态、键盘、焦点、中断、减少动态是否按预期工作 | 大规模用户理解度或所有设备性能 | +| 性能测量 | 指定设备、浏览器和负载下的帧时间、掉帧、长任务、布局/绘制与资源成本 | 用户是否理解或喜欢该效果 | +| 真实用户研究 | 特定人群与任务中的理解、完成效率、错误、干扰与不适 | 未覆盖人群的结论或无条件因果关系 | + +【建议】性能记录注明设备、浏览器、内容规模和并发情境;不把固定帧率口号当作全场景验收线。用户研究区分偏好和实际表现,样本与场景不足时说明局限;没有研究时不宣称“已经提升效率”。 + +### 7.3 最小评价记录 + +【建议】每个有实际用途的动效,只需能简要回答: + +1. 用户要理解什么,静态方案是否已经足够? +2. 为什么选择这个模式、范围和节奏? +3. 进入、退出、重复、中断与取消分别怎样结束? +4. 减少动态、键盘和辅助技术下信息是否完整? +5. 做过哪些设计观察、功能检查和性能测量?哪些仍未验证? +6. 若保留例外,它带来的收益是什么,有什么证据? + +这是一套评价问题,不要求每个轻微反馈都开展完整研究。覆盖范围随交互频率、运动面积、复杂度和用户风险增加。 + +## 8. 来源、版本与复用说明 + +### 8.1 Transitions 官方来源 + +本次研究固定到提交 [`9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62`][commit],提交时间为 2026-10-14 13:48:14 UTC。下列仓库来源及本规范 recipe 链接均固定到该提交。 + +- [官方仓库 README][readme]:定位、展示与可复用 recipes 的关系。 +- [官方 Agent 指南及 recipes 索引][guide]:选择规则、语义化 token、效果边界。 +- [Motion tokens][tokens]:共享尺度与具体效果参数。 +- [官网页面源码][homepage-source]:核对产品展示内容。官网入口为 [Transitions.dev](https://transitions.dev/);本次直连返回 HTTP 403,因此使用固定版本页面源码,未声称已经浏览或验证在线交互效果。 +- 本文链接的九份具体 recipes:modal、accordion、tabs、text swap、skeleton、success、error、tooltip、toggle;用于核对具体表达和边界。 + +【官方】[仓库许可][license] 区分动效/skills 的自定义许可与工具代码的 MIT 许可,不能将整个仓库称为 MIT。动效许可允许在个人及商业产品中使用和修改,限制把集合或其重要部分重新发布为竞争动效库;模板等再分发场景有额外条款。具体复用还应查阅 [Skill 许可][skill-license] 与其指向的完整条款。本规范只保留少量参数、原创归纳和来源链接,不复制或重新分发动效集合;不作超出所读许可的授权承诺。 + +### 8.2 补充来源 + +- [WCAG 2.2][wcag]:本次读取正文中的相关成功准则;各处链接保留条目与等级。 +- [WAI-ARIA APG Tabs][apg-tabs]:组件键盘与语义模式指导。 +- [web.dev:高性能 CSS 动画][performance]:浏览器渲染与测量指导。 + +以上补充网页于 2026-10-15 读取,链接可能后续更新;不作为 Transitions 官方主张。 + +### 8.3 本规范新增了什么 + +【归纳】目的、维度与模式分类来自对官方材料的重新组织;并未把完整效果目录当作基础标准。 + +【建议】连续性与中断策略、静态退化、错误信息保留、验证矩阵和例外判断,是为了形成可长期复用的规范而补充。无障碍和性能结论另标外部来源,不冒充 Transitions 官方保证。 + +采用时,将本文作为动效原则的单一来源;已有基础规范可以通过引用纳入,或将本文内容合并后只保留一个权威版本。具体默认数值及适配决定可另行说明,但不重复维护一套相同原则。上游更新时先核对变化与理由,不因为版本更新就自动替换已经验证的设计选择。 + +[readme]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/README.md +[guide]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/SKILL.md +[tokens]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/_root.css +[modal]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/06-modal.md +[accordion]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/21-accordion.md +[tabs-recipe]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/16-tabs-sliding.md +[text-swap]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/04-text-states-swap.md +[skeleton]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/14-skeleton-reveal.md +[success]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/10-success-check.md +[error]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/12-error-state-shake.md +[tooltip]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/17-tooltip.md +[toggle]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/27-toggle.md +[homepage-source]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/index.html +[license]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/LICENSE +[skill-license]: https://github.com/Jakubantalik/transitions.dev/blob/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62/skills/transitions-dev/LICENSE.txt +[commit]: https://github.com/Jakubantalik/transitions.dev/commit/9cb3a4afe2a8aea0e2fab66fc784fc37830ceb62 +[wcag-motion]: https://www.w3.org/TR/WCAG22/#animation-from-interactions +[wcag-keyboard]: https://www.w3.org/TR/WCAG22/#keyboard +[wcag-order]: https://www.w3.org/TR/WCAG22/#focus-order +[wcag-focus]: https://www.w3.org/TR/WCAG22/#focus-visible +[wcag-status]: https://www.w3.org/TR/WCAG22/#status-messages +[wcag-hover]: https://www.w3.org/TR/WCAG22/#content-on-hover-or-focus +[wcag-flash]: https://www.w3.org/TR/WCAG22/#three-flashes-or-below-threshold +[wcag-pause]: https://www.w3.org/TR/WCAG22/#pause-stop-hide +[wcag]: https://www.w3.org/TR/WCAG22/ +[apg-tabs]: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ +[performance]: https://web.dev/articles/animations-guide diff --git a/.trellis/spec/guides/code-reuse-thinking-guide.md b/.trellis/spec/guides/code-reuse-thinking-guide.md new file mode 100644 index 00000000..72fefa52 --- /dev/null +++ b/.trellis/spec/guides/code-reuse-thinking-guide.md @@ -0,0 +1,14 @@ +# 代码复用判断 + +新增 helper、组件、服务或合并相似代码前,先找已有实现、调用方与测试。 + +- 相似代码是否表达同一业务概念,还是写法碰巧相似? +- 调用方是否应因同一原因一起变化?变化原因不同通常应保留独立实现。 +- 提取后依赖方向是否正确?不要为了共用把业务规则塞入通用 UI 或底层工具。 +- 抽象是否减少理解成本,还是增加开关、可选参数和隐藏状态? +- 有无已有组件、类型、schema、仓储或解析入口可用? +- 修改共享实现影响哪些调用方,怎样证明旧行为保持? + +次数、行数和相似度不是自动抽象阈值。外部数据解析需要明确契约归属,但不因此要求新建通用解码框架。 + +具体规则:[组件](../frontend/component-guidelines.md)、[类型](../frontend/type-safety.md)、[分层](../backend/directory-structure.md)、[测试](../testing/index.md)。 diff --git a/.trellis/spec/guides/cross-layer-thinking-guide.md b/.trellis/spec/guides/cross-layer-thinking-guide.md new file mode 100644 index 00000000..1aebb231 --- /dev/null +++ b/.trellis/spec/guides/cross-layer-thinking-guide.md @@ -0,0 +1,14 @@ +# 跨层修改判断 + +适用于前端、API、CLI、领域、持久化、扫描边界的变化。只沿受影响路径分析。 + +1. 梳理输入→转换→状态变更/存储→响应→消费者,指出契约的权威来源。 +2. Web、CLI、兼容接口、后台任务哪些消费者受影响?生成类型是否要更新? +3. 区分格式校验、身份授权、领域不变量与数据库结构完整性;入口校验不能替代后续授权和状态检查,也不在每层重复相同规则。 +4. 成功、拒绝、超时、取消、重试和并发时,哪些状态已经改变?外部副作用能否回退? +5. 新旧版本并行是否兼容?配置、迁移、应用与消费者的发布顺序是什么? +6. 哪个断言能发现最可能的错误?类型通过能否证明失败后的状态正确? + +按需读:[API](../backend/api-contracts.md)、[事务并发](../backend/transactions-and-concurrency.md)、[请求](../frontend/hook-guidelines.md)、[测试](../testing/index.md)、[部署](../deployment/index.md)。业务状态与权限引用正式 docs/OpenSpec。 + +经验写回所属专题,包含触发、原因、预防、测试锚点。上游 Trellis 平台模板、文档站和升级实现不是 SkillHub 项目规则。 diff --git a/.trellis/spec/guides/index.md b/.trellis/spec/guides/index.md new file mode 100644 index 00000000..057417ba --- /dev/null +++ b/.trellis/spec/guides/index.md @@ -0,0 +1,8 @@ +# 思考指南 + +这里帮助选择适用规范,不另立一套工程规则。先读[总索引](../index.md),只追加当前问题需要的指南。 + +- 新增抽象、组件或合并实现:[代码复用](./code-reuse-thinking-guide.md)。 +- 接口、状态、数据库或多个消费者变化:[跨层分析](./cross-layer-thinking-guide.md)。 + +工程约定和行为要求写入所属领域专题;产品契约留在 docs/OpenSpec。经验需有明确触发条件、可复用预防和实现或测试证据;一次性过程留任务记录。 diff --git a/.trellis/spec/index.md b/.trellis/spec/index.md new file mode 100644 index 00000000..067464b2 --- /dev/null +++ b/.trellis/spec/index.md @@ -0,0 +1,74 @@ +# SkillHub 工程规范索引 + +本目录是 SkillHub 工程约定的权威正文,随代码版本维护;不依赖工程 Skill 自动发现。 +适用 AGENTS 指令仍优先。先选择领域,再读取触发条件对应的专题;不要默认加载整个目录。 + +## 按任务读取 + +| 任务 | 入口 | 条件性追加 | +|---|---|---| +| 页面、组件、状态、请求 | [前端](./frontend/index.md) | 公共接口变化追加后端 API 专题;动效读现有动效正文 | +| Java、接口、持久化、扫描集成 | [后端](./backend/index.md) | 按事务、错误、日志、扫描选专题,业务状态读正式契约 | +| 测试、CI、交付验收 | [测试](./testing/index.md) | 受影响领域的行为验收,跨域取并集 | +| 配置、镜像、发布、迁移 | [部署](./deployment/index.md) | 操作手册与环境检查;读取规范不是部署授权 | +| CLI | [CLI](./cli/index.md) | 后端 API 契约、测试、配置按实际影响追加 | +| 需求、开发、PR、收尾 | [工作流](./workflow/index.md) | 大型需求读 OpenSpec;已有任务收尾读归档 | +| 复用判断或跨层分析 | [思考指南](./guides/index.md) | 按问题选指南,不以问题清单替代契约 | + +实现与检查使用相同的适用正文,检查可追加验收材料。目录存在、Skill可见、hook配置或链接有效, +分别不等于正文已读取、规则已执行或行为已验证。没有任务时直接读取,无需为加载规范创建任务。 + +## 轻量使用方式 + +| 情况 | 使用方式 | +|---|---| +| 改文案、补规范或小修 | 读相关规则,直接修改验证,不强制建任务 | +| 多步功能 | 任务记录保存需求、上下文、进度、验证;尊重用户已有任务创建选择 | +| 大型需求或架构调整 | OpenSpec明确需求设计,Trellis组织执行与接续 | +| 已有权威文档 | 引用完整契约;关键规则可在 AGENTS 与专题重复,修改时检查一致性 | +| 换worktree | 从本机模板显式复用通用规则与工具配置,任务记录各自管理;已跟踪规范以分支内容为准 | + +## 五类内容的职责 + +| 类型 | 应写什么 | +|---|---| +| 工程约定 | 范围、职责、模式、理由、例外 | +| 接口与行为契约 | 输入输出、成功失败、取消、并发、持久化及断言;具体产品语义引用正式来源 | +| 质量要求 | 改动触发条件、检查、可观察结果、证据限制 | +| 项目经验 | 真实触发、根因、预防和回归锚点;一次性过程留任务记录 | +| 思考指南 | 决策问题和规则链接,不暗中新增硬规则 | + +五类是审查维度,不要求每篇填满,也不机械建立五套目录。 +专题按独立适用条件拆分;详细契约有明确归属,关键规则可在 AGENTS 和专题重复,变更时一并核对,不能出现互相冲突的版本。 +规则需有适用范围、例外和源码/测试/设计来源。 +代码现状不自动等于正确标准:分别标明目标、已知偏差和已验证行为,冲突不能静默裁定。 + +## 正文应具体到什么程度 + +索引负责选择,思考指南可以提出问题;实现专题不能只写“注意兼容”“合理测试”“复用现有能力”。 +对本次新增或调整的规则,评审时应能回答: + +- **何时读:** 哪种改动触发它,哪些情况不适用?例如仅改翻译文字不等于修改请求行为。 +- **改哪里:** 指向真实类、函数、配置字段或测试;命名只是建议时明确标注,不虚构已有 helper。 +- **怎么做:** 给出本项目的代表例子,必要时附反例;不把第三方项目的框架和业务规则照搬过来。 +- **怎样判定:** 指出输入与可观察结果,例如错误退出码、缓存是否清空、数据库是否回滚,而非只列测试命令。 +- **哪些尚未做到:** 将现有行为、目标要求和缺口分开;已存在测试不等于本次已运行,计划中的断言不写成已有覆盖。 + +简单命名规则不必填五段模板,复杂专题也不靠凑示例数量证明完整。新增命令应说明工作目录、依赖和副作用。 +规则变化同时核对 AGENTS 中的摘要、所属索引和专题;链接检查只能证明路径可达,内容仍需对照来源审查。 + +## 与其他资料的关系 + +- AGENTS直接保留关键产品、架构、安全与高频工程规则;本目录补充场景、例外、实现依据和验证方法。 +- docs保留架构、业务语义和操作手册;OpenSpec保留正式需求、设计、行为变化与验收。 +- POM、package.json、schema、源码、测试和脚本提供当前事实;规范链接它们,不维护重复版本清单。 +- Trellis执行Skill只组织启动、读取、检查、收尾,不持有另一套项目规范。 +- 学到经验时先判断是否稳定复用;需沉淀则更新所属专题及对应检查,不要求每次任务新增文档。 + +## 版本与本机模板 + +仅 `.trellis/spec/` 纳入Git;任务、journal、运行状态、个人配置和执行工具保持本地。 +公开规范不能依赖私人绝对路径或未提交正文。新检出无需安装Trellis即可阅读本目录。 +本机模板是显式复用与分发工具,不能静默覆盖当前分支已跟踪规范或同步其他worktree的任务。 +本地同步/发布脚本存在时先核对实际范围、来源差异及备份;不因同步绕过AGENTS修改审批。 +归档与清理按[任务记录](./workflow/task-archive.md)处理;无记录不强制创建。 diff --git a/.trellis/spec/testing/backend-testing.md b/.trellis/spec/testing/backend-testing.md new file mode 100644 index 00000000..83fe6c45 --- /dev/null +++ b/.trellis/spec/testing/backend-testing.md @@ -0,0 +1,39 @@ +# 后端测试设计 + +测试放在对应 Maven 模块的 `src/test/java/`。使用现有 JUnit 5、Mockito、AssertJ 与 Spring 测试切片; +沿用 `{ControllerName}Test`、`{ServiceName}Test`、`{FlowName}IntegrationTest`、 +`{ControllerName}SecurityTest` 等相邻命名,不为统一命名重命名现有测试。 + +## 按需要证明的行为选层次 + +| 行为 | 测试边界与断言 | +|---|---| +| 领域状态、权限决策、计算 | 领域单测;正常、边界、拒绝及失败后状态,mock 外部端口而非被测规则 | +| HTTP 参数、认证上下文、响应与异常映射 | Controller / 安全测试;状态码、响应字段、匿名/越权负例;服务单测不能证明过滤器链 | +| JPA 查询、数据库约束、锁、原生 SQL | 仓储或数据库集成测试;检查实际结果与约束,不只 verify repository 调用 | +| 事务回滚、提交后副作用、并发更新 | 独立事务与连接,控制竞争顺序,检查最终持久状态、重复副作用与失败返回 | +| Flyway 迁移 | 适用 PostgreSQL 上运行迁移并验证升级后的 schema/数据;文本扫描仅为辅助护栏 | + +数据库方言、锁或事务是风险来源时,H2 与 mock 不足以代替 PostgreSQL。复用项目的 Testcontainers +模式,或者任务已授权的隔离实例;禁止把测试指向共享/生产库。测试依赖缺失应报告受阻, +不能删除断言或跳过用例后宣称该契约已验证。 + +## 隔离与并发 + +- fixture 显式表达角色、归属、版本和初始状态,避免依赖本机已有数据。 +- 回滚型测试只证明其覆盖的事务;后台线程、异步任务、提交回调需要单独等待与清理。 +- 用 latch/barrier 或明确事务顺序触发竞争,设有界超时;不要依赖机器速度与 sleep。 +- 验证允许的一组结果以及最终不变量,不强求未由业务契约指定的竞争赢家。 +- 涉及 Redis、对象存储或消息时分别证明持久化和外部副作用,测试结束释放资源。 + +## 执行与已知经验 + +使用 `make test-backend-app`(含 `-am`)或 `make test-backend`。 +只运行 `./mvnw -pl skillhub-app clean test` 可能使用本地 Maven 仓库的旧兄弟模块产物, +产生误导性的符号/签名错误。出现这类错误先核对 reactor 范围,不直接修改正确的业务签名。 +过滤单个测试时同样考虑依赖模块及无匹配测试的处理,运行前核对当前 Maven 配置。 + +来源: [Makefile](../../../Makefile)、[测试依赖](../../../server/skillhub-app/pom.xml)、 +[PostgreSQL 邀请并发用例](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/NamespaceInvitationPostgresTest.java)、 +[迁移静态护栏](../../../server/skillhub-app/src/test/java/com/iflytek/skillhub/db/FlywayMigrationGuardrailTest.java)。 +前者可参考真实连接和并发接受邀请的组织方式;后者不替代数据库迁移运行。 diff --git a/.trellis/spec/testing/ci.md b/.trellis/spec/testing/ci.md new file mode 100644 index 00000000..90699b24 --- /dev/null +++ b/.trellis/spec/testing/ci.md @@ -0,0 +1,24 @@ +# CI 检查与证据 + +修改流水线、路径过滤、测试入口或交付门槛时读取。工作流名称不证明它运行了什么, +必须核对 `on`、job/step 条件、矩阵和执行命令。 + +## 读取入口 + +- [pr-tests.yml](../../../.github/workflows/pr-tests.yml):前端构建/单测、后端单测、条件性文档构建。 +- [pr-e2e.yml](../../../.github/workflows/pr-e2e.yml):当前启动开发栈并执行前端 E2E, + 不能沿用“仅 staging smoke”的旧描述。 +- [pr-cli.yml](../../../.github/workflows/pr-cli.yml):CLI 平台矩阵、lint/typecheck/test/build 与 Node 运行验证。 +- [工作流目录](../../../.github/workflows/):脚本、Helm、Scanner 等其他路径按变更追加,不在此复制易过期完整清单。 + +## 质量要求 + +1. 路径过滤要覆盖源代码、共享依赖、锁文件和检查自身;没有触发不等于检查通过。 +2. 修改聚合结果或退出处理时验证成功、失败、跳过和取消传播;不能吞掉失败或仅检查进程启动。 +3. draft 跳过、路径无关跳过、环境失败、用户取消分别报告。只有运行过且断言通过才算相应范围通过。 +4. 需要的分支保护检查以实际仓库配置为准;仅看到 YAML 不声称已设为 required。 +5. 工具版本以各模块清单和工作流实际配置核对;发现不一致记录差异,不在规范整理时顺手升级。 +6. 测试产物、构建产物、镜像发布和真实部署分别证明。产物应关联被检查的提交,旧运行不覆盖后续改动。 + +本地反馈和交付按[选测矩阵](./index.md)执行;CI必需检查仍以实际仓库配置为准。 +本文件不赋予跳过适用门槛、触发部署或发布的权限。 diff --git a/.trellis/spec/testing/e2e.md b/.trellis/spec/testing/e2e.md new file mode 100644 index 00000000..faec8daf --- /dev/null +++ b/.trellis/spec/testing/e2e.md @@ -0,0 +1,30 @@ +# 真实 E2E 与 Smoke + +涉及跨页面业务、认证、权限、发布与运维流程时读取。操作步骤见[现有 E2E 手册](../../../docs/e2e.md), +配置事实以 [Playwright 配置](../../../web/playwright.config.ts)和[Smoke 配置](../../../web/playwright.smoke.config.ts)为准。 +不要复制手册中的固定用例数量、地址或历史执行结果为规范。 + +## 真实链路与数据 + +- `web/e2e` 保持真实请求,不用 `page.route` 拦截关键 API 或伪造业务响应。 + 隔离的组件/视觉 mock 测试可以存在,但应放在适用测试层且明确不是真实后端 E2E。 +- 复用 [session helper](../../../web/e2e/helpers/session.ts) 的认证与 worker 账号隔离; + 数据创建与清理复用[数据 helper](../../../web/e2e/helpers/test-data-builder.ts)。 + 新场景核对 helper 实际清理能力,不假定它会回收所有数据。 +- 测试账号和数据必须专用。记录本次创建的对象,失败时也按精确对象清理,不清空共享数据库。 +- 优先 `getByRole`、`getByLabel`、`getByTestId`;等待 URL、状态或元素条件,不新增盲等。 +- 操作成功需验证用户结果或持久状态;按钮可点、页面可达不能替代完整业务验收。 + +## 执行与环境 + +入口为 `make test-e2e-frontend`、`make test-e2e-smoke-frontend`。 +默认 Playwright 可启动前端,但不会因此自动提供完整后端;先识别 URL、服务和账号来源。 +外部目标使用当前配置支持的 `PLAYWRIGHT_BASE_URL`,不要未经确认指向真实用户环境。 +Smoke 是关键路径子集;通过不能宣称全量覆盖。CI/本地重试后通过应记录首次失败并排查不稳定原因。 + +运维流程变化时更新相应脚本: +[basic](../../../scripts/smoke-test.sh)、[namespace](../../../scripts/namespace-smoke-test.sh)、 +[governance](../../../scripts/governance-smoke-test.sh)、[promotion](../../../scripts/promotion-smoke-test.sh)。 +脚本会产生业务写入,运行前读取参数、账号、创建和清理逻辑。 +认证链排查需核对 cookie jar、登录后 CSRF token 与实际权限,不能靠关闭安全检查让 Smoke 通过。 +报告保留命令、目标环境、版本、失败步骤及脱敏证据;trace、截图和日志不可携带会话凭证公开传播。 diff --git a/.trellis/spec/testing/frontend-testing.md b/.trellis/spec/testing/frontend-testing.md new file mode 100644 index 00000000..9d4b9619 --- /dev/null +++ b/.trellis/spec/testing/frontend-testing.md @@ -0,0 +1,57 @@ +# 前端测试设计 + +适用于组件、Hook、页面状态与请求行为。使用现有 Vitest、Testing Library 和 Playwright, +工具配置以 [package.json](../../../web/package.json)、[Vite 配置](../../../web/vite.config.ts)为准。 + +## 选择边界 + +- 纯转换、校验、格式化用单元测试覆盖输入边界;不要为简单常量或文案写镜像实现的测试。 +- 组件测试从用户动作与可见结果断言,包括错误提示、禁用/重复点击、键盘与焦点; + 需要 DOM 的用例按相邻测试配置 jsdom,不假定全部测试有浏览器环境。 +- Hook 的参数映射、query key、失效范围可以隔离验证;若问题在请求竞争、缓存或重渲染, + 必须保留对应机制进行行为测试,完全 mock React Query 不能证明缓存隔离正确。 +- 浏览器布局、真实导航、认证联动进入 [E2E](./e2e.md);截图只证明捕获状态,不能证明流程完成。 + +## 异步、fixture 与隔离 + +1. 每例建立所需最小状态;清理 DOM、全局 stub、存储、计时器、监听器及 QueryClient 缓存, + 避免一个测试先运行才能通过。不同用户/命名空间用可区分的数据。 +2. 用受控 promise 驱动 A、B 请求乱序完成,断言最终显示 B;涉及取消时验证取消后不覆盖状态, + 不能只断言调用了 abort。卸载、筛选切换、退出登录按修改范围加入。 +3. 乐观更新验证成功确认和失败恢复,尤其不能把期间另一项成功修改回滚掉;没有乐观更新的流程不强加它。 +4. 需要测试时钟时只模拟所需计时器并恢复;等待可观察条件,不能加固定 sleep 掩盖竞态。 +5. fixture 保持真实类型与必要字段,复用已有 helper;测试 mock 不应成为生产类型来源。 + +先运行相关用例,再按[入口矩阵](./index.md)执行类型、lint、单测与构建。 +可见改动还需核对长内容、小屏、键盘与 reduced-motion,具体要求见[前端验收](../frontend/quality-guidelines.md)。 + +## 来源与经验边界 + +[use-promotion-list.test.ts](../../../web/src/features/promotion/use-promotion-list.test.ts) +可参考参数与缓存键断言;目前未断言 mutation 的缓存失效,其 mock 方式也不证明 React Query 的真实竞态行为。 +这是选择测试边界的例子,不要求所有 Hook 使用同一模板。 + +## 可直接执行的三个验证切片 + +以下命令从仓库根目录运行,依赖需已按锁文件安装。只选择与此次修改相关的切片,不因这里列出就全量执行。 + +| 改动 | 命令 | 通过时具体证明什么 | +|---|---|---| +| 晋升列表的分页/排序/key | `pnpm -C web test src/features/promotion/use-promotion-list.test.ts` | PENDING 默认参数、历史排序参数及 ASC/DESC 键区分 | +| 会话缓存清理 | `pnpm -C web test src/features/notification/notification-session.test.ts` | 用户键分区、指定私有缓存移除、公共搜索缓存保留 | +| 客户端路径/运行配置 | `pnpm -C web test src/api/client.test.ts` | 当前测试覆盖的 base URL 路径前缀、端点与运行配置行为;不证明取消或授权闭环 | + +### 会话切换:为什么只断言“调用清理函数”不够 + +实际路径为 `useSessionBootstrap.onSuccess` → `clearSessionScopedQueries` → +`QueryClient.removeQueries`,随后才写入 `['auth', 'me']`。若新增私有查询却未纳入清理/分区机制, +清理函数照常被调用,仍可能留下上一用户的数据。 + +[现有缓存测试](../../../web/src/features/notification/notification-session.test.ts)使用真实 QueryClient: +先放入 A 的通知、标签和个人技能缓存,再清理,断言它们不存在,同时断言公共搜索缓存仍存在。 +修改该机制时沿用这种“清理前有数据 → 清理后私有数据消失、公共数据保留”的正反断言, +并为新增缓存前缀补一项,而不是只检查 mock 函数调用次数。 + +当前测试使用已完成的缓存数据,不包含在途请求或真实登录流程。若改动涉及 A 请求尚未完成时 +切到 B,另用受控 Promise 和真实 QueryClient 验证迟到的 A 数据不会成为 B 的结果。 +不得把现有三个测试通过写成“账号切换、取消和竞态已全部验证”。 diff --git a/.trellis/spec/testing/index.md b/.trellis/spec/testing/index.md new file mode 100644 index 00000000..93fd163d --- /dev/null +++ b/.trellis/spec/testing/index.md @@ -0,0 +1,53 @@ +# 测试与质量入口 + +实现后验证、测试设计、CI 或交付验收时读取。本目录说明怎样选测与执行;领域 +quality-guidelines 说明必须验证的行为,业务预期来自正式设计、接口和验收标准。 + +## 按影响选择检查 + +先运行最小相关检查获得反馈,再执行本次影响范围的交付检查。跨领域修改取并集。 +表中命令从仓库根目录运行;具体测试过滤参数按当前工具和相邻用例核对,不猜测。 + +| 改动与风险 | 检查入口 | 应留下的可观察证据 | +|---|---|---| +| 工程规范与索引 | `python3 scripts/check-engineering-specs.py`;检查命令来源、规则冲突 | 链接目标、迁移去向、检查范围;不是规则加载成功的证明 | +| 仅修改既有翻译资源的显示文案 | 检查资源语法、键/插值一致性与相关界面;影响布局时检查长文本和小屏 | 显示语义与资源消费正确;规范校验器不能替代文案验证 | +| 仅 CLI 帮助显示字符串或说明文档 | 源码帮助字符串执行相关帮助输出测试,确认用法与退出语义;仅文档检查链接和命令来源 | 实际帮助输出或文档引用;不追加无关前后端构建 | +| 前端状态、组件、请求 | `make typecheck-web`、`make lint-web`、`make test-frontend`;交付补 `make build-frontend` | 受影响行为与错误分支通过;可见改动另做浏览器验收 | +| 后端应用与领域规则 | `make test-backend-app` | 当前源码及依赖模块测试结果,包含适用拒绝/失败分支 | +| 共享后端模块、构建依赖、广泛契约 | `make test-backend` | 受影响消费方和全模块结果 | +| SQL、事务、并发、迁移 | 后端检查加[数据库与并发测试](./backend-testing.md) | 在真实依赖上观察提交、回滚、竞争与迁移;mock 不替代 | +| API 契约或生成类型 | `make generate-api`,按需 `./scripts/check-openapi-generated.sh`;前端类型与消费方检查 | schema 差异已审查;响应与错误语义有断言 | +| 身份、权限、操作流程 | 后端边界测试 + [真实 E2E / Smoke](./e2e.md) | 允许与拒绝、跨账号/命名空间隔离、最终持久状态 | +| CLI | `make lint-cli`、`make typecheck-cli`、`make test-cli`、`make build-cli` | 退出码、输出、状态文件和子进程行为;见[CLI](../cli/index.md) | +| 配置、容器与发布路径 | `make validate-release-config` + 受影响构建/Smoke | 配置生效与业务路径分开证明;见[部署](../deployment/index.md) | +| CI 与检查脚本 | [CI 规范](./ci.md) + 代表性通过/失败/跳过输入 | 能检出错误,失败正确传递,没有误报通过 | + +上述文案例外仅适用于不改变键、插值参数、分支、事件、样式、参数解析或生成逻辑的显示文本。 +一旦涉及这些行为或工具链,升级到对应领域检查;跨域取并集,不仅凭文件扩展名决定范围。 +文档站内容另核实际构建/CI要求。既有相关断言失败要调查修正,不能以“纯文案”为由跳过失败。 + +`make generate-api` 读取运行中后端并写生成文件。`check-openapi-generated.sh` 会启动 +Compose 依赖和后端、写生成文件,并在退出时执行 `docker compose down`;必须先检查 +目标项目、端口、已有服务和清理范围,使用隔离环境,不能当作纯静态检查。 +`make staging` 会构建并启动容器、执行可写 Smoke,亦不是静态检查。 + +## 提交前门槛 + +[AGENTS.md](../../../AGENTS.md) 与[CONTRIBUTING](../../../CONTRIBUTING.md)采用上表的按影响检查。 +纯文档执行文档验证;代码改动覆盖相关行为;跨域取并集,不将快速反馈等同完整交付验证。 +容器、运行配置或集成链路变化时追加 `make staging` 或覆盖同等受影响路径的隔离集成检查, +明确选择依据、环境和断言。安全、权限、事务或并发行为不能只靠类型或编译通过验收。 +环境受阻时报告未执行项目及原因,不把受阻等同通过;CI必需检查仍须按实际仓库配置满足。 + +## 专题与证据 + +- [前端测试](./frontend-testing.md):组件、Hook、异步与隔离。 +- [后端测试](./backend-testing.md):领域、HTTP、真实数据库与事务。 +- [E2E 与 Smoke](./e2e.md):真实会话、数据和清理。 +- [CI](./ci.md):触发条件、状态、产物和本地检查的关系。 +- [前端验收](../frontend/quality-guidelines.md)、[后端验收](../backend/quality-guidelines.md):领域行为要求。 + +报告写实际命令、检出版本、范围、通过/失败/未运行、环境限制;不要用编译替代运行, +用截图存在替代目视检查,用旧提交结果替代当前修改。缺陷修复的测试应能区分修复前后行为。 +来源:[Makefile](../../../Makefile)、[PR 模板](../../../.github/pull_request_template.md)。 diff --git a/.trellis/spec/workflow/development-flow.md b/.trellis/spec/workflow/development-flow.md new file mode 100644 index 00000000..6e938d58 --- /dev/null +++ b/.trellis/spec/workflow/development-flow.md @@ -0,0 +1,37 @@ +# 开发与交付流程 + +## 轻量使用分层 + +| 情况 | 使用方式 | +|---|---| +| 文案、小修或补规范 | 读取相关规则,直接修改验证,不强制建任务 | +| 多步功能 | 使用任务记录保存需求、上下文、进度、验证;尊重用户已有不创建任务的选择 | +| 大型需求或架构调整 | [OpenSpec](./openspec.md)明确正式需求设计,Trellis组织执行接续 | +| 已有权威文档 | 引用详细契约,关键规则可在 AGENTS 重复;变更时核对入口与正文一致 | +| 换worktree | 从本机模板显式同步可复用规则及工具配置,任务各自管理;已跟踪spec以分支内容为准,模板更新必须比较后显式应用 | + +## 实现与交接 + +从[总索引](../index.md)选择领域与专题,无需工程 Skill 自动触发。需求不清先澄清。 +实现前明确行为差距、影响文件、来源与验证方式。实现和检查使用同一组正文,检查再追加[测试入口](../testing/index.md)。 +有任务时复用现有上下文清单;无任务时直接传递路径与范围,不补建任务。 +文件存在、Skill可见或hook配置不等于实际读取。 + +## 开发环境 + +命令以[Makefile](../../../Makefile)和[开发手册](../../../docs/dev-workflow.md)为准,版本查POM/package.json。 +启动停止先确认服务与数据,重置、推送、PR、发布不是普通检查。热更新不替代验证。 +并行环境见[手册](../../../docs/13-parallel-workflow.md),不默认操作其他实例。 + +## 检查与PR + +按测试入口区分快速反馈与交付门槛。API变化检查消费者和生成类型,运维变化检查Smoke,文档检查引用。 +提交前检查diff、生成文件和敏感信息;不自动提交、推送、创建PR或部署。 +标题和提交采用 `(): `,type为feat/fix/docs/test/refactor/chore,scope为受影响领域。 +一次提交一个逻辑变化。PR说明问题、改变后的行为、必要实现细节、验证及兼容影响,参考[贡献指南](../../../CONTRIBUTING.md)与[PR模板](../../../.github/pull_request_template.md)。 +评审给适用规范和源码证据,不以个人偏好替代规则。 + +## 收尾 + +区分通过、失败、未运行和环境受阻。复用经验写所属专题,普通修复不强制新增规则。 +已有任务或清理worktree时读[归档](./task-archive.md);不存在记录不为归档补建任务。 diff --git a/.trellis/spec/workflow/grill-with-docs.md b/.trellis/spec/workflow/grill-with-docs.md new file mode 100644 index 00000000..da88acf6 --- /dev/null +++ b/.trellis/spec/workflow/grill-with-docs.md @@ -0,0 +1,53 @@ +# Grill With Docs + +Clarify requirements when misunderstood terms, boundaries, or trade-offs would make implementation risky. +`$grill-with-docs` is an optional local aid when installed; this workflow does not require any Skill installation. + +## Position in the Workflow + +Run this before OpenSpec or implementation planning when any of these are true: + +- The user describes a large feature in broad terms. +- Domain terms are overloaded or not yet canonical. +- The design has several meaningful branches. +- The decision may create ADR-worthy trade-offs. +- Future agents will need the reasoning, not just the final tasks. + +Recommended sequence: + +1. Clarify the unsettled requirements directly, or use an available clarification Skill +2. OpenSpec change for large requirements +3. Trellis task for multi-step execution context when authorized; small edits can proceed directly +4. Backend/frontend/workflow specs for implementation + +## What the Skill Does + +Some local setups compose two skills behind `$grill-with-docs`; inspect the installed entry rather than assuming it exists: + +- `$grilling`: asks frontier questions as a decision tree until the design is no longer silently + assuming key choices. +- `$domain-modeling`: sharpens domain language, updates `CONTEXT.md` when terms are resolved, and + creates ADRs only for hard-to-reverse, surprising, trade-off decisions. + +If these skills are unavailable, clarify directly: identify the concrete user outcome, ambiguous terms, +affected actors and systems, success/failure cases, constraints and unresolved trade-offs. Ask only questions +whose answers materially change the work. Summarize decisions and select task size using the development flow. + +## SkillHub Documentation Policy + +- Do not create `CONTEXT.md` just to satisfy a process. Create it only when a SkillHub-specific term + is resolved and worth reusing. +- Do not create `docs/adr/` just to record a normal implementation choice. ADRs require a real, + costly trade-off. +- OpenSpec remains the place for formal product behavior, acceptance scenarios, and task breakdown. +- `CONTEXT.md` is glossary only. It is not an implementation spec or scratch pad. + +## Handoff Output + +At the end of a grill session, capture: + +- Settled decisions. +- Remaining open decisions. +- Canonical terms and avoided synonyms, if any. +- ADR candidates accepted or explicitly skipped. +- Whether the next artifact is an OpenSpec change, a Trellis task, or a direct edit. diff --git a/.trellis/spec/workflow/index.md b/.trellis/spec/workflow/index.md new file mode 100644 index 00000000..b948f501 --- /dev/null +++ b/.trellis/spec/workflow/index.md @@ -0,0 +1,28 @@ +# Workflow Guidelines + +This layer explains how SkillHub chooses between direct edits, Trellis tasks, and OpenSpec changes. +These shared conventions are readable without installing Trellis. Local execution tools are optional. +Respect an existing user choice about task creation; do not repeatedly ask or silently create a task. + +## Pre-Development Checklist + +- Product capability or scope changes: read [Product Direction](../../../docs/00-product-direction.md); preserve registry-first positioning and compatibility requirements. +- Read `.trellis/spec/index.md` for the unified routing map. +- Read [Grill With Docs](./grill-with-docs.md) when the requirement, domain language, or design + boundary is still fuzzy. +- Read [OpenSpec Workflow](./openspec.md) for large requirements. +- Read [Development Flow](./development-flow.md) for task-size routing and verification expectations. +- For implementation work, also read the relevant domain index selected by the root routing map, including CLI when applicable. + +## Quality Check + +- Does this task have the right container: direct edit, Trellis task, or OpenSpec change? +- If the requirement was fuzzy, did `$grill-with-docs` settle the decision tree before planning? +- If an OpenSpec change exists, did implementation stay aligned with its proposal/spec/design/tasks? +- Did verification read [Testing and Quality](../testing/index.md) and the relevant domain checks? +- Did runtime/release work read [Deployment](../deployment/index.md) before executing commands? + +## Task History + +Finish/cleanup and cross-worktree history lookup follow [Local Task Archive](./task-archive.md). +When no record exists for this task, do not create one merely to archive it. Preserve unrelated task records. diff --git a/.trellis/spec/workflow/openspec.md b/.trellis/spec/workflow/openspec.md new file mode 100644 index 00000000..308ab4c0 --- /dev/null +++ b/.trellis/spec/workflow/openspec.md @@ -0,0 +1,34 @@ +# OpenSpec Workflow + +OpenSpec remains the formal workflow for large SkillHub requirements. + +Authoritative source: + +- `openspec/` +- Existing example: `openspec/changes/add-skill-suites/` + +## When to Use OpenSpec + +Use or create `openspec/changes//` for large requirements or architecture decisions, such as: + +- A substantial new product capability or large user-facing workflow. +- Architecture or module-boundary change. +- Breaking or broad public API, persistence, security, or lifecycle changes requiring coordinated design and acceptance. + +An ordinary multi-step feature uses task records; changing one endpoint or adding one test does not +automatically require a new OpenSpec change. If an accepted OpenSpec already owns the affected behavior, +update that source rather than creating a competing contract. Preserve the user's existing task-creation choice. + +## Expected Artifacts + +Each large change should have: + +- `proposal.md`: why the change exists and what problem it solves. +- `specs//spec.md`: behavior deltas and acceptance scenarios. +- `design.md`: technical approach and important trade-offs. +- `tasks.md`: implementation and verification checklist. + +## Trellis Relationship + +Trellis tasks may link to an OpenSpec change. Trellis should not replace OpenSpec for formal +requirements. If task work changes accepted behavior, update the OpenSpec source first. diff --git a/.trellis/spec/workflow/task-archive.md b/.trellis/spec/workflow/task-archive.md new file mode 100644 index 00000000..94c222cb --- /dev/null +++ b/.trellis/spec/workflow/task-archive.md @@ -0,0 +1,14 @@ +# 任务记录与归档 + +适用于已有任务的收尾和worktree清理;任务、运行状态、个人报告不属于公开规范。 + +- 按已配置的本地执行入口读取归档工具说明,保存当前单个任务的摘要、原始记录与证据链接。 +- 摘要说明结果、仓库/分支、提交或PR(若有)、验证范围与未完成项;未提交和未验证如实记录。 +- 清理前保存到worktree外,验证目标可读、内容完整。失败保留源记录,不继续依赖归档成功的清理。 +- 检查凭证和私人数据;链接报告优于复制源码和环境文件。归档不授予提交、推送或删除权限。 +- 稳定任务写入后再快照,不覆盖同标识但不同内容的记录。 +- 未配置工具时不假设本机命令存在,说明缺口并保留原记录。没有任务记录不强制创建。 + +工具路径、参数和私有档案由本地工作流或执行Skill配置,不成为公开规范硬依赖。 +原生archive可能修改状态、移动目录、触发hooks或提交;运行前核对,不把它等同跨worktree备份。 +验证观察已保存摘要和原始记录;Git clean不证明忽略的任务文件已经保存。 diff --git a/AGENTS.md b/AGENTS.md index 729a1b59..cd5c226a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,593 +1,99 @@ -# SkillHub — AGENTS.md - -**SkillHub** is an **enterprise-grade, self-hosted agent skill registry** for publishing, -discovering, and managing reusable skill packages across an organization. It provides a **REST API -backend**, a **React web UI**, a **security scanner**, and a **ClawHub CLI compatibility layer**. - -## Quick Reference - -| Item | Value | -|------------|------------------------------------------------------------| -| Backend | Spring Boot 3.2.3, Java 21, Maven multi-module (7 modules) | -| Frontend | React 19, TypeScript, Vite, pnpm | -| Scanner | Python (FastAPI), port 8000 | -| Database | PostgreSQL 16 (Flyway migrations) | -| Cache | Redis 7 (sessions, distributed locks, idempotency) | -| Storage | LocalFile (dev) / S3/MinIO (prod) | -| Build | `make dev-all` (dev), `make staging` (pre-PR) | -| Docs | `docs/` (design), `docs/skillhub/` (VitePress user guide) | -| CI | GitHub Actions (`.github/workflows/`) | - -## Directory Map - -``` -skillhub/ -├── server/ # Maven multi-module Spring Boot backend -│ ├── skillhub-app/ # Application layer: bootstrap, controllers, assembly -│ │ ├── bootstrap/ # Bootstrap admin & local dev data initializers -│ │ ├── compat/ # ClawHub CLI compatibility layer controllers -│ │ ├── config/ # Spring configuration classes -│ │ ├── controller/ # REST controllers (transport only) -│ │ │ ├── admin/ # Admin controllers (user mgmt, labels, search) -│ │ │ ├── portal/ # Portal controllers (skills, governance, security) -│ │ │ └── support/ # Package extractors (zip, multipart) -│ │ ├── dto/ # Request/response DTOs -│ │ ├── exception/ # Exception handling -│ │ ├── filter/ # Servlet filters (auth context, rate limiting) -│ │ ├── listener/ # Event listeners (notification recipients, etc.) -│ │ ├── metrics/ # Micrometer metrics -│ │ ├── projection/ # Lifecycle projection models -│ │ ├── ratelimit/ # Rate limiting logic -│ │ ├── repository/ # Query repositories (read-model assembly) -│ │ ├── security/ # Security configuration -│ │ ├── service/ # App services (workflow orchestration) -│ │ ├── stream/ # SSE streaming endpoints -│ │ ├── task/ # Background task scheduling -│ │ └── SkillhubApplication.java # Spring Boot entry point -│ │ -│ ├── skillhub-domain/ # Domain layer: entities, rules, services (innermost) -│ │ ├── audit/ # AuditLog entity, repository, service -│ │ ├── auth/ # Password reset entities -│ │ ├── event/ # Domain event classes (SkillPublishedEvent, etc.) -│ │ ├── governance/ # Governance notification service -│ │ ├── idempotency/ # Idempotency records -│ │ ├── label/ # Skill label management -│ │ ├── namespace/ # Namespace, members, roles, policies -│ │ ├── report/ # Skill reporting/governance -│ │ ├── review/ # Review tasks, promotion requests -│ │ ├── security/ # Security scanning domain model -│ │ ├── shared/ # Shared domain utilities -│ │ │ └── exception/ # Domain exceptions (LocalizedDomainException, etc.) -│ │ ├── skill/ # Core skill entities and services -│ │ │ ├── metadata/ # SKILL.md frontmatter parsing -│ │ │ ├── service/ # Skill domain services (publish, query, governance) -│ │ │ └── validation/ # Package validation (SkillPackagePolicy, etc.) -│ │ ├── social/ # Star, rating, subscription entities -│ │ └── user/ # UserAccount, profile moderation -│ │ -│ ├── skillhub-auth/ # Authentication & authorization -│ │ ├── config/ # Spring Security configuration -│ │ ├── device/ # OAuth Device Flow for CLI auth -│ │ ├── identity/ # Identity binding service -│ │ ├── local/ # Local (password) auth -│ │ ├── merge/ # Account merging -│ │ ├── oauth/ # OAuth2 login handlers -│ │ ├── policy/ # Route security policies -│ │ ├── rbac/ # RBAC service and role definitions -│ │ ├── token/ # API token management -│ │ └── user/ # User-related auth services -│ │ -│ ├── skillhub-search/ # Search SPI + PostgreSQL full-text implementation -│ │ ├── postgres/ # PostgresFullTextIndexService, QueryService -│ │ └── service/ # Search SPI interfaces -│ │ -│ ├── skillhub-storage/ # Object storage SPI -│ │ ├── local/ # LocalFileStorageService -│ │ └── s3/ # S3StorageService (AWS SDK v2) -│ │ -│ ├── skillhub-infra/ # Infrastructure: JPA repos, utilities -│ │ └── repository/ # Spring Data JPA repository implementations -│ │ -│ ├── skillhub-notification/ # Notification service (SSE, email) -│ │ ├── domain/ # Notification domain model -│ │ ├── service/ # Notification delivery services -│ │ └── sse/ # SSE endpoint support -│ │ -│ ├── Dockerfile.dev # Dockerfile for staging builds -│ ├── Dockerfile # Production multi-stage build -│ ├── pom.xml # Parent POM (Spring Boot 3.2.3 parent) -│ └── scripts/ -│ └── run-dev-app.sh # Local dev startup script -│ -├── web/ # React frontend (Vite + pnpm) -│ ├── src/ -│ │ ├── api/ # OpenAPI-generated types + fetch client -│ │ │ └── generated/ -│ │ │ └── schema.d.ts # Generated OpenAPI types (CHECKED IN) -│ │ ├── app/ # Router, layout, global providers -│ │ ├── docs/ # In-app documentation pages -│ │ ├── entities/ # Domain entity display logic -│ │ │ ├── skill/ # Skill card, detail components -│ │ │ ├── user/ # User profile components -│ │ │ └── namespace/ # Namespace display components -│ │ ├── features/ # Business feature modules -│ │ │ ├── admin/ # Admin panel features -│ │ │ ├── auth/ # Login, OAuth flows -│ │ │ ├── governance/ # Skill governance actions -│ │ │ ├── namespace/ # Namespace management -│ │ │ ├── notification/ # User notifications -│ │ │ ├── promotion/ # Skill promotion workflows -│ │ │ ├── publish/ # Skill upload/publish UI -│ │ │ ├── report/ # Skill reporting -│ │ │ ├── review/ # Review workflow UI -│ │ │ ├── search/ # Skill search and filtering -│ │ │ ├── security-audit/ # Security audit viewer -│ │ │ ├── skill/ # Skill detail, listing -│ │ │ ├── social/ # Stars, ratings, subscriptions -│ │ │ └── token/ # API token management -│ │ ├── i18n/ # Internationalization -│ │ ├── pages/ # Route-level page components -│ │ ├── shared/ # Shared UI, hooks, utilities -│ │ │ ├── components/ # Reusable UI components -│ │ │ ├── hooks/ # Custom React hooks -│ │ │ ├── lib/ -│ │ │ │ └── utils.ts # cn() class merging utility -│ │ │ └── ui/ # Radix UI-based primitives -│ │ └── types/ # Additional TypeScript types -│ ├── e2e/ # Playwright E2E tests -│ ├── nginx.conf.template # Nginx runtime config template -│ ├── Dockerfile # Multi-stage build (Node → Nginx) -│ └── package.json # Dependencies (React 19, TanStack Query, Radix UI, etc.) -│ -├── scanner/ # Security scanner (Python/FastAPI) -│ ├── docs/ # Scanner documentation -│ ├── examples/ # Example scan inputs/outputs -│ └── Dockerfile # Scanner container build -│ -├── docs/ # Design documents (source of truth) -│ ├── prds/ # Product requirement documents -│ ├── skillhub/ # VitePress user guide source -│ └── superpowers/ # Internal tooling docs -│ -├── deploy/k8s/ # Kubernetes manifests (basic) -├── monitoring/ # Prometheus + Grafana stack -├── scripts/ # Build, test, and deployment scripts -│ ├── smoke-test.sh # Basic API smoke test -│ ├── namespace-smoke-test.sh # Namespace workflow smoke test -│ ├── governance-smoke-test.sh # Governance flow smoke test -│ ├── promotion-smoke-test.sh # Promotion flow smoke test -│ ├── check-openapi-generated.sh # Verify OpenAPI SDK is not stale -│ ├── validate-release-config.sh # Validate release env configuration -│ ├── dev-process.sh # Local process manager (PID-based) -│ ├── runtime.sh # Runtime deployment script -│ ├── parallel-init.sh # Parallel worktree initialization -│ ├── parallel-sync.sh # Merge worktrees in integration branch -│ ├── parallel-up.sh # Merge + start dev environment -│ ├── parallel-down.sh # Stop parallel dev environment -│ └── prepare-pr-batch.sh # Batch PR preparation -│ -├── .github/ -│ ├── workflows/ # GitHub Actions CI/CD -│ ├── ISSUE_TEMPLATE/ # Issue templates -│ └── scripts/ # Deno scripts for triage, release notes, rewards -│ -├── AGENTS.md # AI agent rules (this file) -├── .agents/skills/ # Focused AI skill definitions -├── Makefile # Top-level build/test/dev orchestration -├── docker-compose.yml # Local dev dependency services -├── compose.release.yml # Production release compose file -├── CONTRIBUTING.md # Contribution guidelines -├── CODE_OF_CONDUCT.md # Community standards -└── README.md # Project overview -``` - -**Key Locations for Common Tasks:** - -| Task | Where to Look | -|------|---------------| -| Add REST endpoint | `server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/` | -| Add domain entity/service | `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/` | -| Add auth logic | `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/` | -| Add search logic | `server/skillhub-search/src/main/java/com/iflytek/skillhub/search/` | -| Add query repository | `server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/` | -| Change RBAC/roles | `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/` | -| Change skill validation | `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/` | -| Add frontend page | `web/src/pages/` | -| Add frontend feature | `web/src/features/` | -| Add shared component | `web/src/shared/components/` | -| Change API contract | Backend controller → run `make generate-api` → commit generated file | -| Add smoke test | `scripts/` (new `.sh` file) | -| Add E2E test | `web/e2e/` (Playwright) | -| Add backend test | `server/skillhub-*/src/test/java/` (alongside source module) | - -## Critical Rules - -### Do Not Manually Edit Generated Files - -- `web/src/api/generated/schema.d.ts` — regenerated via `make generate-api` -- `server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/` — some DTOs may be generated - -### After Making Changes - -**Backend changes:** -- Edit Java code → `make dev-server-restart` (local dev) -- Add/modify controller → `make generate-api` to regenerate frontend types -- Add/modify domain service → `make test-backend-app` to verify tests - -**Frontend changes:** -- Edit TypeScript/React → Vite HMR handles reload automatically -- After `make generate-api` → commit updated `web/src/api/generated/schema.d.ts` - -**Always run before PR:** -```bash -make test-backend-app # Backend tests (with dependent modules) -make typecheck-web # Frontend type check -make lint-web # Frontend lint -make staging # Full staging regression + smoke test -``` - -### File-Specific Requirements - -- **Controllers** (`skillhub-app/controller/`) are transport only: extract auth context, - bind request params, wrap responses. No business logic. -- **App Services** (`skillhub-app/service/`) orchestrate workflows. Do not embed complex - read-model assembly here — extract to query repositories. -- **Query Repositories** (`skillhub-app/repository/`) handle read-model joins and presentation - projection. Named like `*QueryRepository`. -- **Domain Services** (`skillhub-domain/*/service/`) contain business rules and state transitions. - Return domain objects, not DTOs. -- **Repository Interfaces** are defined in `skillhub-domain`, implemented in `skillhub-infra`. -- **Domain Exceptions** use `LocalizedDomainException` for user-facing messages with i18n keys. -- **Package-info files** (`package-info.java`) should exist for all packages. - -## Development Workflow - -### Build & Start - -```bash -make dev-all # Start full stack: Postgres, Redis, MinIO, backend, frontend -make dev-all-down # Stop everything -make dev-all-reset # Full reset (clears data volumes) -make dev-status # Check service status -make dev-server-restart # Restart backend after Java changes -``` - -**Access points:** -- Web UI: `http://localhost:3000` -- Backend API: `http://localhost:8080` -- Scanner: `http://localhost:8000` - -**Local mock users** (no password needed): - -| User ID | Role | Header | -|---------|------|--------| -| `local-user` | Regular user | `X-Mock-User-Id: local-user` | -| `local-admin` | Super admin | `X-Mock-User-Id: local-admin` | - -**Bootstrap admin** (password-based, local profile): -- Username: `admin` / Password: `ChangeMe!2026` -- Disable with `BOOTSTRAP_ADMIN_ENABLED=false` - -### Lint & Format - -```bash -# Backend: enforced by Maven build (no separate lint target) -# Frontend: -make lint-web # ESLint check -make typecheck-web # TypeScript check -``` - -### Testing - -```bash -make test-backend-app # Backend unit tests (skillhub-app + dependencies) -make test-backend # All backend module tests -make test-frontend # Frontend unit tests (Vitest) -make test-e2e-frontend # Frontend E2E tests (Playwright) -make test-e2e-smoke-frontend # Frontend E2E smoke tests -./scripts/smoke-test.sh # API smoke test -make namespace-smoke # Namespace workflow smoke test -``` - -### Staging (Pre-PR Regression) - -```bash -make staging # Build backend Docker image + frontend static + smoke test -make staging-down # Tear down -SERVICE=web make staging-logs # View Nginx logs -``` - -Staging validates the containerized deployment path: -- Backend: built as Docker image from local source -- Frontend: built as static files, served by Nginx -- Dependencies: same Postgres/Redis/MinIO as local dev - -### Parallel Agent Workflow - -For parallel development with isolated worktrees: - -```bash -make parallel-init TASK=feature-name -``` - -Creates dedicated Claude, Codex, and integration worktrees as sibling directories. -See `docs/13-parallel-workflow.md` for details. - -## PR Submission - -### PR Title Format - -Use conventional commit style: - -``` -(): -``` - -**Types:** -- `feat`: New feature -- `fix`: Bug fix -- `docs`: Documentation changes -- `test`: Adding or updating tests -- `refactor`: Code restructuring (no behavior change) -- `chore`: Build, CI, or maintenance tasks - -**Scopes:** Module or domain name (e.g., `auth`, `search`, `publish`, `review`, `namespace`) - -**Examples:** -``` -feat(auth): add local account login -fix(publish): resolve null pointer in skill validation -docs(deploy): clarify runtime image usage -test(namespace): add membership service tests -refactor(review): extract query repository for governance list -chore(ci): add parallel workflow scripts -``` - -### Pre-PR Checklist - -- [ ] Backend tests pass: `make test-backend-app` -- [ ] Frontend typecheck passes: `make typecheck-web` -- [ ] If API changed: `make generate-api` was run and `web/src/api/generated/schema.d.ts` is committed -- [ ] Smoke test passes: `make staging` -- [ ] Follow existing module boundaries and dependency direction -- [ ] Add/update tests for new behavior -- [ ] Update docs when APIs, auth flows, deployment, or operator workflows change - -## Core Concepts - -### Backend Clean Architecture - -``` -app → domain, auth, search, storage, infra, notification -infra → domain # implements domain repository interfaces -auth → domain -search → domain -notification → domain -storage → (independent) # pure SPI -``` - -**Design intent**: `skillhub-domain` is the innermost layer. It defines entities, repository -interfaces, and domain services without depending on infra, auth, search, or storage. - -**Code reality**: `skillhub-domain` declares a Maven dependency on `skillhub-storage`, and several -domain services (`SkillHardDeleteService`, `SkillDownloadService`, `SkillPublishService`, -`SkillGovernanceService`, `SkillQueryService`, `SkillStorageDeletionCompensationService`) import -`com.iflytek.skillhub.storage.ObjectStorageService`. This is an existing deviation from the ideal. -New code should avoid adding further cross-module dependencies from domain. - -### Repository / Query Boundary - -When adding new read logic, follow these rules: - -1. **Domain repository ports** (`skillhub-domain`): Aggregate reads, state transitions, rule - evaluation. Used by domain services. -2. **App query repositories** (`com.iflytek.skillhub.repository`): Read-model assembly that joins - multiple sources, presentation projection. Used by controllers and app services. -3. **App services** (`com.iflytek.skillhub.service`): Workflow orchestration. Should express "what - this endpoint does", not "how it assembles DTOs". -4. **Direct SQL / EntityManager**: Only when necessary, with class-level comment explaining why. - -**Do not** add complex read-model assembly logic inside app services. Extract it into a query -repository when it joins multiple sources, does presentation projection, or is reused across services. - -### Skill Lifecycle - -`SkillVersionStatus` values: `DRAFT`, `SCANNING`, `SCAN_FAILED`, `UPLOADED`, `PENDING_REVIEW`, -`PUBLISHED`, `REJECTED`, `YANKED`. - -`SkillStatus` enum values: `ACTIVE`, `HIDDEN`, `ARCHIVED`. - -The design doc (`docs/14-skill-lifecycle.md`) specifies that `hidden` should be treated as a -governance overlay rather than a lifecycle state. The current code still defines -`SkillStatus.HIDDEN` in the enum. Follow the design doc's intent for new code. - -**Key transitions:** -- Normal user first upload → `PENDING_REVIEW` (no initial DRAFT) -- SUPER_ADMIN first upload → `PUBLISHED` (direct publish) -- Review approve → `PENDING_REVIEW` → `PUBLISHED` (updates `latestVersionId`) -- Review reject → `PENDING_REVIEW` → `REJECTED` -- Withdraw review → `PENDING_REVIEW` → `UPLOADED` (also deletes pending review_task) -- Yank → `PUBLISHED` → `YANKED` (must recalculate `latestVersionId`) -- Hide/restore → independent `hidden` flag (governance overlay) -- Archive/Unarchive → `ACTIVE` ↔ `ARCHIVED` (container state) - -### Namespace Coordinate System - -SkillHub uses `@{namespace_slug}/{skill_slug}`: -- `@global/my-skill` — Platform-level public namespace -- `@my-team/my-skill` — Team/department namespace - -ClawHub CLI compatibility maps: -| SkillHub | Canonical Slug | -|----------|---------------| -| `@global/my-skill` | `my-skill` | -| `@team-name/my-skill` | `team-name--my-skill` | - -### Authentication - -- Web: OAuth2 (GitHub) + local password auth -- CLI: OAuth Device Flow (web authorization → CLI credentials) -- Programmatic: API tokens (prefix-based secure hashing) -- Session: Spring Session + Redis - -### RBAC - -Platform roles: `SUPER_ADMIN`, `SKILL_ADMIN`, `USER_ADMIN`, `AUDITOR` -Namespace roles: `OWNER`, `ADMIN`, `MEMBER` - -### Skill Package Protocol - -- Root: `SKILL.md` with YAML frontmatter (`name`, `description` required) -- Allowed extensions (50+ types): `.md`, `.txt`, `.json`, `.yaml`, `.yml`, `.js`, `.ts`, `.py`, - `.sh`, `.png`, `.jpg`, `.svg`, and many more (see `SkillPackagePolicy.ALLOWED_EXTENSIONS`) -- Limits: 10MB per file, 100MB total, 500 files max -- File type signatures validated (PNG magic bytes, SVG content check, etc.) - -### Frontend State Management - -- **TanStack Query** (`@tanstack/react-query`): All server state (API data) -- **Zustand**: Local/UI state (theme, sidebar, modals) -- **Never** use `useEffect` for data fetching - -### Frontend Component Composition - -- **Radix UI** primitives: `@radix-ui/react-dropdown-menu`, `@radix-ui/react-select` -- **class-variance-authority** (cva) for component variants -- **clsx** + **tailwind-merge** for class merging -- **`cn()` utility**: `web/src/shared/lib/utils.ts` -- shadcn/ui is NOT used as a library — only Radix primitives + utility composition - -## Common Patterns - -### Code Style - -**Java:** -- User identity type is always `String` throughout the codebase -- Use Java 21 features (records, pattern matching, virtual threads) -- Follow existing naming patterns in the domain layer -- Error strings for `DomainBadRequestException`, etc. should be clear and actionable - -**TypeScript:** -- Strict mode. No `any` types. -- Use generated OpenAPI types for all API interactions. -- Feature-Sliced Design: place code at the lowest appropriate layer. - -### Testing Philosophy - -- Backend: JUnit 5 + Mockito + AssertJ -- Frontend: Vitest for unit tests, Playwright for E2E -- **Use `make test-backend-app`** (includes `-am` for dependent modules) — never run - `./mvnw -pl skillhub-app clean test` directly, as it can use stale Maven cache artifacts -- Test behaviors, not implementations -- Use Spring Boot test slices where possible (`@WebMvcTest`, `@DataJpaTest`) - -### Frontend Testing - -```bash -make test-frontend # Vitest unit tests -make test-e2e-frontend # Playwright E2E -make test-e2e-smoke-frontend # Playwright smoke (subset of E2E) -``` - -### Logging Conventions - -- **Backend**: SLF4J + Spring Boot logging. Use structured logging with MDC for request tracing. -- **Frontend**: `console.error` for errors, `console.warn` for deprecations, avoid `console.log` in production code. -- **Scanner**: Python logging module with structured JSON output. - -### Security - -- API tokens are stored as prefix-based secure hashes, never in plaintext -- OAuth2 client secrets and other secrets must not be logged or committed -- User identity is `String` (supports external SSO/OIDC/SCIM identity sources) -- The bootstrap admin (`BOOTSTRAP_ADMIN_ENABLED`) is for zero-config quickstart only - -## Search Tips - -```bash -# Find all REST endpoints -rg "@(Get|Post|Put|Delete|Patch)Mapping" --type java - -# Find domain services -rg "class.*Service" server/skillhub-domain/ - -# Find query repositories -rg "QueryRepository" server/skillhub-app/ - -# Find controllers -rg "@RestController" server/skillhub-app/ - -# Find RBAC role checks -rg "@PreAuthorize" server/skillhub-app/ - -# Find skill validation logic -rg "SkillPackage" server/skillhub-domain/ - -# Find frontend features -rg "export" web/src/features/ - -# Find OpenAPI type generation script -rg "generate-api" web/package.json - -# Find event listeners -rg "@EventListener" server/ -``` - -## Design Philosophy - -- **Hub first**: The server-side registry is the core product; CLI and agent integrations are entry capabilities -- **Compatibility first**: Support `SKILL.md` format and common directory conventions -- **Layered architecture**: Search and object storage must have replaceable boundaries (SPI pattern) -- **Open authentication**: OAuth2-based, extensible to multiple providers beyond GitHub -- **Audit first**: Enterprise distribution requires audit trails for publish, download, delete, and authorization - -## References - -### Essential Files -- **`Makefile`** — All build/test/dev automation targets -- **`CONTRIBUTING.md`** — Contribution guidelines and commit style -- **`CODE_OF_CONDUCT.md`** — Community standards -- **`server/pom.xml`** — Maven parent POM, module definitions, dependency versions -- **`web/package.json`** — Frontend dependencies and scripts -- **`.github/workflows/pr-tests.yml`** — PR test pipeline -- **`.github/workflows/publish-images.yml`** — Docker image publish to GHCR - -### Key Directories -- **`server/skillhub-domain/`** — Core domain (entities, services, rules) -- **`server/skillhub-app/controller/`** — REST API endpoints -- **`server/skillhub-app/repository/`** — Query repositories -- **`server/skillhub-app/compat/`** — ClawHub CLI compatibility layer -- **`server/skillhub-auth/`** — Authentication and authorization -- **`web/src/features/`** — Frontend feature modules -- **`web/src/api/generated/`** — Generated OpenAPI types -- **`docs/`** — Design documents -- **`scripts/`** — Build, test, and deployment scripts - -### Important Scripts -- **`scripts/smoke-test.sh`** — Basic API smoke test -- **`scripts/namespace-smoke-test.sh`** — Namespace workflow test -- **`scripts/check-openapi-generated.sh`** — Verify frontend SDK is current -- **`scripts/validate-release-config.sh`** — Validate production env config -- **`scripts/dev-process.sh`** — Local process manager (PID-based lifecycle) -- **`scripts/parallel-init.sh`** — Create isolated worktrees for parallel development - -### Design Documents -- **`00-product-direction.md`** — Product positioning, MVP scope, coordinate system -- **`01-system-architecture.md`** — System architecture, module structure, dependency rules -- **`02-domain-model.md`** — Domain entities and relationships -- **`03-authentication-design.md`** — OAuth2, CLI Device Flow, API tokens -- **`04-search-architecture.md`** — Search SPI and implementations -- **`05-business-flows.md`** — Business process flows -- **`06-api-design.md`** — API contract specifications -- **`07-skill-protocol.md`** — SKILL.md format, package structure, CLI compatibility -- **`08-frontend-architecture.md`** — Frontend patterns and conventions -- **`14-skill-lifecycle.md`** — Skill state model (authoritative) -- **`dev-workflow.md`** — Local development workflow guide - -### External Resources -- **SkillHub Docs**: https://iflytek.github.io/skillhub/ -- **DeepWiki**: https://deepwiki.com/iflytek/skillhub -- **Discord**: https://discord.gg/qHYvtDNPHS -- **OpenSkills**: https://agents.md/ (skill package format reference) -- **OpenClaw**: https://github.com/openclaw/openclaw (CLI compatibility) -- **AstronClaw**: https://agent.xfyun.cn/astron-claw (cloud AI assistant integration) +# SkillHub — Agent Entry + +SkillHub includes a Java backend, React web UI, CLI and security-scanning integration. +Read [README](./README.md) for setup and [CONTRIBUTING](./CONTRIBUTING.md) for contributions. + +## Read before changing code + +1. Start with [.trellis/spec/index.md](./.trellis/spec/index.md). +2. Read only the applicable domain index and the topic documents selected by the change. +3. Implementation and review use the same applicable contracts; add the testing guidance for verification. +4. Read actual source, tests, schemas and scripts before treating documentation as implemented behavior. + Report conflicts and known deviations; do not silently redefine business policy. + +| Change | Entry | +|---|---| +| Frontend UI, state, requests, types | [Frontend](./.trellis/spec/frontend/index.md) | +| Backend modules, APIs, persistence, scanning | [Backend](./.trellis/spec/backend/index.md) | +| Tests, CI, acceptance | [Testing](./.trellis/spec/testing/index.md) | +| Configuration, containers, release, migrations | [Deployment](./.trellis/spec/deployment/index.md) | +| CLI | [CLI](./.trellis/spec/cli/index.md) | +| Development, PR, completion | [Workflow](./.trellis/spec/workflow/index.md) | +| Reuse or cross-layer decisions | [Thinking guides](./.trellis/spec/guides/index.md) | + +## Core project rules + +- The registry is the core product; CLI and agent integrations are entry points. +- Preserve SKILL.md package compatibility and existing namespace/CLI contracts. +- Keep search and object storage behind replaceable interfaces. +- Authentication must remain extensible beyond a single identity provider. +- Changes to publishing, downloading, deletion or authorization must check + that the required audit trail is preserved. + +## Backend essentials + +- Controllers handle transport and trusted identity extraction; application + services orchestrate use cases; domain services own business rules. +- Put complex joins and presentation projections in app query repositories. + Domain services return domain objects, not app DTOs. +- Domain repository interfaces belong in domain; persistence adapters belong + in infra. Do not introduce new dependencies that violate module boundaries. +- The existing domain-to-storage dependency is a known deviation, not a + precedent for adding further cross-module dependencies. +- User identity is String throughout authentication, APIs and persistence. + This does not imply that every resource ID is String. +- Use existing localized domain exceptions and request-tracing facilities. +- Store API tokens using the existing secure hashing mechanism, never plaintext. + +## Frontend essentials + +- Use TanStack Query for server state and Zustand/local state for UI state. + Do not introduce useEffect-based API fetching. +- Reuse existing shared UI components, variants and cn() utilities. +- Use generated API types; do not hand-edit generated schema files. +- Keep user-facing text in translation resources. +- Use console.error for errors and console.warn for deprecation warnings; + avoid production console.log debugging and never log credentials. + +## High-value checks + +- For backend app tests, use make test-backend-app, or an equivalent Maven + invocation that includes -am, to avoid stale dependent-module artifacts. +- API contract changes must check generated types and affected consumers. +- Lifecycle changes must compare docs, source and tests; report disagreements + rather than silently changing business policy. +- Search changes: read docs/04-search-architecture.md. +- Business-flow changes: read docs/05-business-flows.md. + +## Work proportionately + +- Small edits: read relevant rules, edit and verify; do not require a task record. +- Multi-step work: use a task record for requirements, context, progress and verification, respecting an existing user choice not to create one. +- Large requirements or architecture changes: OpenSpec owns formal requirements and design; Trellis organizes execution and continuation. +- Repeat critical rules here when they should be visible at session entry. + Keep detailed contracts in the relevant specs and authoritative documents. + When changing a repeated rule, check both locations for consistency. +- Each worktree owns its task records. Tracked specs follow the checked-out branch; local template updates are explicit and must not silently overwrite them. + +## Verification and delivery + +Choose checks using the [change-impact matrix](./.trellis/spec/testing/index.md). +Start with focused feedback; broaden for shared behavior, public interfaces, configuration and integration changes. +Documentation-only changes require relevant document/source checks, not starting the application stack. +Report passed, failed, not-run and environment-blocked checks separately. +A build, unit test, screenshot or static configuration check is not proof of deployment or end-to-end behavior. + +Use the existing Makefile and package tooling. Do not hand-edit generated OpenAPI types. +Read commands before execution: staging and schema-drift checks may start/stop services. +Preserve unrelated changes; do not commit, push, publish, deploy or remove worktrees without authorization. +Do not commit or log credentials; do not reset data to make verification pass. + +## Sources and maintenance + +- Engineering conventions: [.trellis/spec/](./.trellis/spec/index.md). +- Architecture, business semantics and operations: [docs/](./docs/), including [lifecycle](./docs/14-skill-lifecycle.md). +- Formal changes: [OpenSpec workflow](./.trellis/spec/workflow/openspec.md). +- Versions, commands and current behavior: dependency manifests, Makefile, schemas, source and tests. +- Only `.trellis/spec/` is shared in Git. Tasks, runtime state and personal Trellis tooling remain local. +- Execution Skills may help run a workflow; they do not own a second copy of project conventions. +- Preserve and verify existing task records before authorized cleanup; see [archive requirements](./.trellis/spec/workflow/task-archive.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b5b5df3..221272e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,10 @@ preserve the existing architecture and product direction documented in [`docs/`](./docs). AI coding agents working in this repository should follow the rules in -[`AGENTS.md`](./AGENTS.md), which documents repository architecture, -dependency rules, and agent-specific conventions. +[`AGENTS.md`](./AGENTS.md), which routes to the engineering conventions in +[`.trellis/spec/index.md`](./.trellis/spec/index.md). These documents can be read +without installing Trellis or any engineering Skill. Architecture and business +design remain in `docs/`; execution tools and task records remain local. ## Before You Start @@ -63,7 +65,10 @@ Before opening a pull request, make sure: - The branch is rebased or merged cleanly from the target branch. - Relevant backend tests pass. -- Frontend typecheck/build passes when frontend files changed. +- Frontend checks follow the [change-impact matrix](.trellis/spec/testing/index.md). + Changes limited to existing translation display text use its narrow exception; + frontend behavior or tooling changes require the applicable typecheck/build checks. + This does not waive required CI checks or failures in relevant existing tests. - `make generate-api` or `./scripts/check-openapi-generated.sh` has been run when backend API contracts changed. - Smoke coverage is updated when operator-facing workflows change. diff --git a/scripts/check-engineering-specs.py b/scripts/check-engineering-specs.py new file mode 100644 index 00000000..bff32a36 --- /dev/null +++ b/scripts/check-engineering-specs.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Check portable engineering specs and references to retired engineering Skills.""" +import re +import sys +from pathlib import Path +from urllib.parse import unquote, urlsplit + +RETIRED = ( + "api-and-namespace-design", "backend-module-structure", "code-conventions", + "dev-workflow", "frontend-conventions", "pr-submission", "skill-lifecycle", + "testing-and-ci", +) +LINK = re.compile(r"(? tuple[list[str], int, int]: + spec = root / ".trellis/spec" + errors = [] + files = sorted(spec.rglob("*.md")) + if not (spec / "index.md").is_file(): + errors.append("missing .trellis/spec/index.md") + paths = files + [root / name for name in ("AGENTS.md", "CONTRIBUTING.md")] + links = 0 + for path in paths: + if not path.is_file(): + errors.append(f"missing {path.relative_to(root)}") + continue + body = path.read_text(encoding="utf-8") + label = path.relative_to(root) + for name in RETIRED: + if f".agents/skills/{name}" in body: + errors.append(f"{label}: reference to retired Skill {name}") + if path in files and re.search(r"/(?:home|Users|tmp)/|(?") + parts = urlsplit(target) + if parts.scheme or parts.netloc or not parts.path: + continue + links += 1 + target_path = Path(unquote(parts.path)) + resolved = (path.parent / target_path).resolve() + if not resolved.is_relative_to(root): + errors.append(f"{label}: link escapes checkout: {target}") + elif not resolved.exists(): + errors.append(f"{label}: missing link target: {target}") + elif resolved.is_relative_to(root / ".trellis") and not resolved.is_relative_to(spec): + errors.append(f"{label}: link depends on local Trellis state: {target}") + for name in RETIRED: + if (root / ".agents/skills" / name).exists(): + errors.append(f"retired engineering Skill still present: {name}") + return errors, len(files), links + + +def main() -> int: + root = Path(__file__).resolve().parent.parent + errors, files, links = check(root) + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + print(f"Engineering specs: {files} documents, {links} local links checked.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())