# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. > **Cross-repo rules** — see `/Users/wen/project/rag/CLAUDE.md` for full workspace conventions. Key shared rules: > - File-scoped namespaces, primary constructor DI, `record` for DTOs/Commands/Queries > - Entity: `BaseEntity + IAuditable/ISoftDelete/IFullAudit` marker interfaces > - EF Config: snake_case table, `ValueGeneratedNever()`, `IEntityTypeConfiguration` > - Endpoint: `FastEndpoint` + `Permissions("resource:action")` > - Middleware: `Cors → GlobalException → ApiResponse → Auth → AuthZ → FastEndpoints` > - Response: `{ code: 0, data, message: "ok" }` (auto-wrapped) > - JWT shared key: `RagJwtSecretKey2026MustBeAtLeast32CharsLong!` > - Other repos: `rag-backend` (5211), `im-system` (5212), `work-flow`, `file-system` (8080 Go), `rag-frontend` (5666 Vue) ## Build & Run ```bash dotnet build cd src/Workflow.Api && dotnet run cd src/Workflow.Api && dotnet run -- --seed cd src/Workflow.Infrastructure && dotnet ef migrations add --startup-project ../Workflow.Api dotnet test # xUnit + FluentAssertions + Moq ``` ## Architecture .NET 10, four projects: `Api → Application → Infrastructure → Domain`. Same ABP+FastEndpoints+MediatR stack as rag-backend, but with workflow-specific engine patterns. ### Key Difference: Domain-Driven State Machines The core domain logic lives in **stateless state machines**, not in entities. Entities are anemic data holders. Three state machines in `Domain/StateMachine/`: - `InstanceStateMachine` — Running ↔ Suspended → Completed/Terminated - `TaskStateMachine` — Pending → Approved/Rejected/Transferred/Delegated - `TokenStateMachine` — Active → Consumed/Terminated Pattern: tuple switch with context objects: ```csharp return (currentState, operation) switch { (InstanceStatus.Running, InstanceOperation.Suspend) => InstanceStatus.Suspended, (TaskStatus.Pending, TaskOperation.Approve) => TaskStatus.Approved, _ => throw new InvalidStateTransitionException(...) }; ``` ### ProcessEngine — Token Propagation `Application/Engine/ProcessEngine.cs` — routes tokens through 7 node types: | Node Type | Behavior | |-----------|----------| | Start | Consumes token, creates new token on first outgoing edge | | End | Consumes token, checks if all instance tokens consumed → Complete | | Approval | Creates `WorkflowTask`, token stays Active until human action | | Cc | Creates notification tasks, immediately propagates token (fire-and-forget) | | Condition | Evaluates edge conditions (first-match-wins), creates one new token | | Parallel | Fork: one token per outgoing edge. Join: waits for all incoming tokens | | SubProcess | Creates child instance, parent token waits | ### Condition Evaluation — Strategy Chain ``` ConditionEvaluator → IValueComparatorRegistry (chain-of-responsibility) → NumericComparator (==, !=, >, <, >=, <=) → DateTimeComparator (==, !=, >, <, >=, <=) → BooleanComparator (==, !=) → CollectionComparator (in) → StringComparator (==, !=, contains, startsWith, endsWith, isEmpty) ``` Two condition syntaxes: simple text (`"amount > 5000"`, regex-parsed) and JSON tree (`{"and": [...], "or": [...]}`). ### Node Action Hooks ```csharp public interface INodeAction { Task ExecuteAsync(NodeActionContext context); } ``` Config specifies `"onEnter": "send-notification"`. Engine resolves from DI. Hook failures are swallowed — never block token propagation. ## Testing ``` tests/Workflow.Tests/ Condition/ — 6 test files (one per comparator + evaluator + registry) Engine/ — ProcessEngineTests (~775 lines, #region-grouped) Form/ — Form definition + data tests with fixture Handlers/ — CQRS handler tests with InMemory DB StateMachine/ — One test file per state machine (pure unit tests) ``` Stack: xUnit + FluentAssertions + Moq + EF Core InMemory. Handler test pattern: ```csharp await using var db = new WorkflowDbContext(new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options); var handler = new CreateXxxCommandHandler(db); var result = await handler.Handle(command, CancellationToken.None); result.Should().NotBeNull(); ``` ## Code Patterns Same conventions as rag-backend (see workspace CLAUDE.md), plus: - Table prefix: `wf_` (e.g., `wf_workflow_definitions`, `wf_workflow_tasks`) - Request DTOs co-located in endpoint files (not separate DTO files) - Endpoints use `AllowAnonymous()` on all routes (auth via gRPC to rag-backend, not local) - Entity audit fields are explicit (no base audit class): `Guid CreatedBy`, `DateTime CreatedAt` - `AuditInterceptor` registered via `OnConfiguring` override, not DI `AddInterceptors`