diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 770f1bc5..32e6ab26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Docs-first development: feature specs in `docs/epics/` are the contract; code im ## Before you push 1. Walk **Gates 0–3** in [docs/playbooks/agent-checklist.md](docs/playbooks/agent-checklist.md) locally; tick the matching boxes in the PR body. -2. When `src/`, `tests/`, or `docs/epics/` change: run `./scripts/check-doc-drift.sh` (bash — use Git Bash on Windows). CI job **Doc drift** must be green. +2. When `src/`, `tests/`, or `docs/epics/` change: run `./scripts/check-doc-drift.sh` (bash — use Git Bash on Windows). CI job **Doc drift** must be green. When editing feature files (`docs/epics/**/features/*.md`), use the layout in [docs/epics/_template-feature-us.md](docs/epics/_template-feature-us.md) and run `python3 scripts/normalize-feature-docs.py --check` (also invoked by the drift script). 3. PR description: **Summary + Linked spec + Requirements only** — no commit list, no CI status (the Checks tab covers that). Template: [.github/PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md). 4. When adding or changing `.proto` files: register the module `Protos` path in [`buf.yaml`](buf.yaml), run `buf lint`, and `./scripts/check-buf-modules.sh` (included in **Doc drift**). CI job **Protobuf — Buf lint and breaking** runs on proto/`buf.yaml` changes — see [patterns.md § gRPC](docs/playbooks/patterns.md). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 4ba1d1f0..ff45daa0 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -47,7 +47,7 @@ Custom model, field, data class, and record CRUD. Full-text search, per-field JS ## WorkflowBuilder — E04-workflow-builder -**Domain ✅ | Application ✅ | Infrastructure ✅ | API ✅ | Frontend ⏳ · Service-boundary retrofit ⚠️** +**Domain ✅ | Application ✅ | Infrastructure ✅ | API ✅ | Frontend ⏳ · Service-boundary retrofit ✅** Workflow definitions with steps, transitions, triggers, cycle detection, publish/archive lifecycle. Import/export (JSON + ZIP). All endpoints covered by integration tests. @@ -55,7 +55,7 @@ Workflow definitions with steps, transitions, triggers, cycle detection, publish ## FormBuilder — E05-form-builder -**Domain ✅ | Application ✅ | Infrastructure ✅ | API ✅ | Frontend ⏳ · Service-boundary retrofit ⚠️** +**Domain ✅ | Application ✅ | Infrastructure ✅ | API ✅ | Frontend ⏳ · Service-boundary retrofit ✅** Form definitions + F04 form tasks (`FormSubmission`, token submit, my tasks, expiry job). Submission user resolved via `ICurrentUser` in Application. @@ -63,17 +63,17 @@ Form definitions + F04 form tasks (`FormSubmission`, token submit, my tasks, exp ## WorkflowEngine — E06-workflow-engine -**Domain ✅ | Application ✅ | Infrastructure ⚠️ | API ✅ | Frontend ⏳ · Service-boundary retrofit ⚠️** +**Domain ✅ | Application ✅ | Infrastructure ⚠️ | API ✅ | Frontend ⏳ · Service-boundary retrofit ✅** -Execution lifecycle (start, cancel, retry, retry-with-context). `ExecutionEndpoints` registered; default-input shaping handled in `StartExecutionHandler`. Infrastructure ⚠️: `IScriptExecutor` and `INotificationSender` stubs. +Execution lifecycle (start, cancel, retry, retry-with-context). `ExecutionEndpoints` registered; default-input shaping handled in `StartExecutionHandler`. Database `axis_workflowengine` with EF migrations; tests use `MigrateAsync`. Infrastructure ⚠️: real `IScriptExecutor` and `INotificationSender` still stubs. -> ⚠️ **Retrofit (PR for E05+E06 closure):** `Axis.WorkflowEngine.Contracts` shipped with Avro schema `FormStepReachedEvent` (CloudEvents envelope, ADR-019). `WorkflowEngineEventMapper` translates domain events at `SaveChangesAsync` time; FormBuilder consumes via Kafka topic `axis.workflowengine.form-step-reached`. The 2 cross-module Domain references that were tracked in `WORKAROUNDS.md` are now resolved (ratchet shrunk). **Deferred:** gRPC service for sync RPC needs (none today); saga orchestrator ([ADR-020](TECH_STACK.md#adr-020-saga-orchestration-for-cross-module-workflows)); dedicated `axis_workflowengine` database; switch tests to `MigrateAsync`; real `IScriptExecutor` + `INotificationSender`. +> ✅ **Retrofit:** `Axis.WorkflowEngine.Contracts` with Avro `FormStepReachedEvent` (CloudEvents, ADR-019). `WorkflowEngineEventMapper` + Kafka outbox; FormBuilder consumes `axis.workflowengine.form-step-reached`. Cross-module Domain references in `WORKAROUNDS.md` resolved. **Deferred:** gRPC (no sync RPC needs today); saga orchestrator ([ADR-020](TECH_STACK.md#adr-020-saga-orchestration-for-cross-module-workflows)); trigger handlers (schedule/webhook/event); SignalR live updates; error-notification dispatch. ## E01 — Platform Foundation **F01 tenant registration (backend):** ✅ register, verify (opaque tokens), resend limit, idempotency, Kafka provisioning coordinator, optional `subscriptionPlanId` on register. -**F04 subscription plans (backend):** ⚠️ `GET /api/plans`, platform plan change, 402 limits (workflows / users / executions), Redis counters. Frontend pricing UI ⏳. Bulk multi-workflow import limit AC deferred until API exists. +**F04 subscription plans (backend):** ✅ `GET /api/plans`, platform plan change, 402 limits (workflows / users / executions), Redis counters. Frontend pricing UI ⏳. **Deferred:** atomic execution counter under concurrency; fail-closed when Redis unavailable; bulk multi-workflow import limit AC until bulk endpoint exists. **F02 organization management (backend):** ✅ US-005–007 — profile API, settings + usage, scheduled deletion with 30-day hard-delete job. Frontend ⏳. @@ -95,4 +95,4 @@ Execution lifecycle (start, cancel, retry, retry-with-context). `ExecutionEndpoi **Status: ✅ Tooling complete — feature implementation ⏳** -React 18 + TypeScript + Vite. TanStack Router, TanStack Query, Zustand, shadcn/ui, Tailwind. Biome (lint + format). `npm run ci` gate enforced. All module feature UIs remain ⏳. +React 19 + TypeScript + Vite. TanStack Router, TanStack Query, Zustand, shadcn/ui, Tailwind. Biome (lint + format). `npm run ci` gate enforced. All module feature UIs remain ⏳. diff --git a/docs/README.md b/docs/README.md index 6951c7c7..ec0bd05b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ | [Product Vision](./PRODUCT_VISION.md) | Goals, target users, problem & solution | | [Tech Stack](./TECH_STACK.md) | Technology decisions and rationale | | [Architecture](./ARCHITECTURE.md) | System design, modules, data strategy | -| [Epics](./epics/README.md) | All epics, features, and user stories — **[how agents find open work](./epics/README.md#how-agents-find-open-work)** | +| [Epics](./epics/README.md) | All epics, features, and user stories — **[how agents find open work](./epics/README.md#how-agents-find-open-work)** · [US template](./epics/_template-feature-us.md) | | [Wireframes](./wireframes/) | Screen wireframes — Excalidraw source + SVG preview | ### Playbooks (how-to guides) @@ -69,7 +69,7 @@ All diagrams are Excalidraw (`.excalidraw` source + `.svg` preview). Regenerate ## Wireframes -Excalidraw wireframes live in `docs/epics/{E0N}/wireframes/`, co-located with each epic's features and diagrams. Shared screens (template, app-shell) remain in `docs/wireframes/`. Each feature file links to its wireframe via a `> **Wireframe**` callout directly after the feature title. +Excalidraw wireframes live in `docs/epics/{E0N}/wireframes/`, co-located with each epic's features and diagrams. Shared screens (template, app-shell) remain in `docs/wireframes/`. Each feature file lists wireframes in a `## Wireframes` table (see [US template](./epics/_template-feature-us.md)). | Screen | Source | Preview | |---|---|---| @@ -83,6 +83,7 @@ When two docs disagree, the **owner** wins. Update the owner first; everything e | Topic | Owner | |---|---| +| Feature US layout (wireframes + status tables) | [epics/_template-feature-us.md](./epics/_template-feature-us.md) + [playbooks/docs-style.md](./playbooks/docs-style.md#feature-files--wireframes--implementation-status) | | Product scope, target users, MVP cut | [PRODUCT_VISION.md](./PRODUCT_VISION.md) | | Library versions and ADRs | [TECH_STACK.md](./TECH_STACK.md) | | Source tree and module boundaries | [../CLAUDE.md](../CLAUDE.md) | diff --git a/docs/diagrams/container.excalidraw b/docs/diagrams/container.excalidraw index 40395b79..3682d60a 100644 --- a/docs/diagrams/container.excalidraw +++ b/docs/diagrams/container.excalidraw @@ -871,7 +871,7 @@ "type": "text", "width": 495.00000000000006, "height": 14, - "text": "React 18 + TypeScript + Vite · shadcn/ui · React Flow · dnd-kit · TanStack Query · Zustand", + "text": "React 19 + TypeScript + Vite · shadcn/ui · React Flow · dnd-kit · TanStack Query · Zustand", "fontSize": 10, "fontFamily": 1, "textAlign": "center", diff --git a/docs/diagrams/container.svg b/docs/diagrams/container.svg index 485ddd89..6a61b1ee 100644 --- a/docs/diagrams/container.svg +++ b/docs/diagrams/container.svg @@ -1,2 +1,2 @@ Axis Platform — Container DiagramAPI Server — ASP.NET Core 8 Modular MonolithIdentityDataModelingWorkflowBuilderFormBuilderWorkflowEnginePageBuilderWolverine — Event Bus + Durable Outbox (per-module)In-process · At-least-once delivery · Per-module outbox tablesOpenIddict 5.xOAuth2/OIDC · Auth Code + PKCESignalRReal-time execution statusWeb ApplicationReact 18 + TypeScript + Vite · shadcn/ui · React Flow · dnd-kit · TanStack Query · ZustandPer-Module Databases (PostgreSQL 16)axis_identitypublic schemaaxis_dmtenant schema per orgaxis_wbwolverine outboxaxis_wewolverine outboxaxis_fbwolverine outboxRedis 7Cache · Session · Schema nameAWS S3File storageEmail ServiceSMTP · MailKit \ No newline at end of file + @font-face { font-family: Virgil; src: url(data:font/woff2;base64,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); }Axis Platform — Container DiagramAPI Server — ASP.NET Core 8 Modular MonolithIdentityDataModelingWorkflowBuilderFormBuilderWorkflowEnginePageBuilderWolverine — Event Bus + Durable Outbox (per-module)In-process · At-least-once delivery · Per-module outbox tablesOpenIddict 5.xOAuth2/OIDC · Auth Code + PKCESignalRReal-time execution statusWeb ApplicationReact 19 + TypeScript + Vite · shadcn/ui · React Flow · dnd-kit · TanStack Query · ZustandPer-Module Databases (PostgreSQL 16)axis_identitypublic schemaaxis_dmtenant schema per orgaxis_wbwolverine outboxaxis_wewolverine outboxaxis_fbwolverine outboxRedis 7Cache · Session · Schema nameAWS S3File storageEmail ServiceSMTP · MailKit \ No newline at end of file diff --git a/docs/diagrams/generate-diagrams.mjs b/docs/diagrams/generate-diagrams.mjs index bc831fb1..27d148c9 100644 --- a/docs/diagrams/generate-diagrams.mjs +++ b/docs/diagrams/generate-diagrams.mjs @@ -324,7 +324,7 @@ function systemContext() { els.push(...rect({ x: 230, y: 65, w: 600, h: 650, bg: "#f0f9ff", stroke: C.sysBdr, label: "Axis Platform", labelSize: 14, labelBold: true, labelColor: C.sysBdr })); // Inside platform — col 1 (left) - els.push(...rect({ x: 268, y: 120, w: 240, h: 70, bg: C.sysBg, stroke: C.sysBdr, label: "Web Application", sub: "React 18 + TypeScript" })); + els.push(...rect({ x: 268, y: 120, w: 240, h: 70, bg: C.sysBg, stroke: C.sysBdr, label: "Web Application", sub: "React 19 + TypeScript" })); els.push(...rect({ x: 268, y: 250, w: 240, h: 70, bg: C.sysBg, stroke: C.sysBdr, label: "API Server", sub: "ASP.NET Core 8 · Modular Monolith" })); els.push(...rect({ x: 268, y: 395, w: 240, h: 70, bg: C.infraBg, stroke: C.infraBdr, label: "PostgreSQL 16", sub: "Per-module databases" })); els.push(...rect({ x: 268, y: 505, w: 240, h: 70, bg: C.infraBg, stroke: C.infraBdr, label: "Redis 7", sub: "Cache · Session" })); @@ -399,7 +399,7 @@ function containerDiagram() { // Web Application band (bottom of platform) els.push(...rect({ x: 50, y: 545, w: 760, h: 55, bg: C.sysBg, stroke: C.sysBdr, label: "Web Application", - sub: "React 18 + TypeScript + Vite · shadcn/ui · React Flow · dnd-kit · TanStack Query · Zustand" })); + sub: "React 19 + TypeScript + Vite · shadcn/ui · React Flow · dnd-kit · TanStack Query · Zustand" })); // DB column (right side; arrows from platform right edge x=810 → DB left edge x=870, 60px each) const DBX = 870, DBW = 190, DBH = 55, DBGap = 10; diff --git a/docs/diagrams/system-context.excalidraw b/docs/diagrams/system-context.excalidraw index f781893b..0774f462 100644 --- a/docs/diagrams/system-context.excalidraw +++ b/docs/diagrams/system-context.excalidraw @@ -457,7 +457,7 @@ "type": "text", "width": 115.50000000000001, "height": 14, - "text": "React 18 + TypeScript", + "text": "React 19 + TypeScript", "fontSize": 10, "fontFamily": 1, "textAlign": "center", diff --git a/docs/diagrams/system-context.svg b/docs/diagrams/system-context.svg index 01a69320..80cbf63d 100644 --- a/docs/diagrams/system-context.svg +++ b/docs/diagrams/system-context.svg @@ -1,2 +1,2 @@ Axis Platform — System Context👤Org Admin[builds workflows & forms]👤End User[submits forms, views pages]Axis PlatformWeb ApplicationReact 18 + TypeScriptAPI ServerASP.NET Core 8 · Modular MonolithPostgreSQL 16Per-module databasesRedis 7Cache · SessionWolverineEvent bus · Durable outboxAWS S3File storageEmail ServiceSMTP / MailKitExternal APIsHTTP Request stepsHTTPSHTTPSREST / WS \ No newline at end of file + @font-face { font-family: Virgil; src: url(data:font/woff2;base64,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); }Axis Platform — System Context👤Org Admin[builds workflows & forms]👤End User[submits forms, views pages]Axis PlatformWeb ApplicationReact 19 + TypeScriptAPI ServerASP.NET Core 8 · Modular MonolithPostgreSQL 16Per-module databasesRedis 7Cache · SessionWolverineEvent bus · Durable outboxAWS S3File storageEmail ServiceSMTP / MailKitExternal APIsHTTP Request stepsHTTPSHTTPSREST / WS \ No newline at end of file diff --git a/docs/epics/E01-platform-foundation/README.md b/docs/epics/E01-platform-foundation/README.md index 48e828a1..1a7c1631 100644 --- a/docs/epics/E01-platform-foundation/README.md +++ b/docs/epics/E01-platform-foundation/README.md @@ -52,10 +52,10 @@ Without this foundation, nothing else works. Every feature in every other epic r | Shared Application | ✅ Done | `ICommand/IQuery`, `ICommandHandler/IQueryHandler`, `ValidationBehavior`, `ITenantContext` | | Shared Infrastructure | ✅ Done | `TenantSchemaInterceptor`, per-module `UnitOfWork` ([ADR-017](../../TECH_STACK.md#adr-017-axisshared-is-abstractions-only-no-shared-implementation)); **OpenTelemetry** host wiring on `Axis.Api` ([ADR-018](../../TECH_STACK.md#adr-018-opentelemetry-sdk-with-grafana-stack-for-observability), [patterns § OpenTelemetry](../../playbooks/patterns.md#opentelemetry-observability)) | | Tenant Registration (US-001–004 backend) | ⚠️ Partial | US-001–002 + plan on register (US-004 backend): opaque verify tokens, resend limit, optional `subscriptionPlanId` (default Free). Frontend ⏳ | -| Subscription Plans (F04 US-010–012 backend) | ⚠️ Partial | `GET /api/plans`, 402 limits (workflow / user / execution), Redis read-through counters, platform plan change. Frontend pricing UI ⏳ | +| Subscription Plans (F04 US-010–012 backend) | ✅ Done | `GET /api/plans`, 402 limits (workflow / user / execution), Redis read-through counters, platform plan change. Frontend pricing UI ⏳. **Deferred:** atomic execution counter; fail-closed Redis. | | Tenant Provisioning (US-003) | ⚠️ Partial | Kafka-driven per-module provisioning with `TenantSchemaProvisioner` helper, `TenantModuleProvisionReportEvent`, Identity coordinator (retry + alert), `tenant_module_provisions` tracking, `GET /api/auth/provisioning-status`. Frontend wait screen ⏳. | | Tenant isolation (F03 US-008–009) | ⚠️ Partial | `TenantSchemaInterceptor` + `HttpTenantContext` + `FixedTenantContext` for jobs; unit tests. Gaps: cross-tenant integration tests, schema Redis cache, deleted-org 403 — see [F03](./features/F03-tenant-isolation.md) | -| Organization Management (F02 US-005–007 backend) | ⚠️ Partial | Profile, settings + usage, scheduled deletion + hard-delete job ✅. Frontend settings UI ⏳ — see [F02](./features/F02-organization-management.md) | +| Organization Management (F02 US-005–007 backend) | ✅ Done | Profile, settings + usage, scheduled deletion + hard-delete job ✅. Frontend settings UI ⏳ — see [F02](./features/F02-organization-management.md) | | Frontend | ⏳ Pending | Registration, verify, provisioning, settings, pricing | --- diff --git a/docs/epics/E01-platform-foundation/features/F01-tenant-registration.md b/docs/epics/E01-platform-foundation/features/F01-tenant-registration.md index abee8934..ac2b2df3 100644 --- a/docs/epics/E01-platform-foundation/features/F01-tenant-registration.md +++ b/docs/epics/E01-platform-foundation/features/F01-tenant-registration.md @@ -2,14 +2,22 @@ [← Back to E01](../README.md) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/register-org.excalidraw](../wireframes/register-org.excalidraw) · [preview](../wireframes/register-org.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/register-org-states.excalidraw](../wireframes/register-org-states.excalidraw) · [preview](../wireframes/register-org-states.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/email-confirmation.excalidraw](../wireframes/email-confirmation.excalidraw) · [preview](../wireframes/email-confirmation.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/verify-email.excalidraw](../wireframes/verify-email.excalidraw) · [preview](../wireframes/verify-email.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/verify-email-rate-limit.excalidraw](../wireframes/verify-email-rate-limit.excalidraw) · [preview](../wireframes/verify-email-rate-limit.svg) -> **Wireframe** (US-002 sign-in before verify): [docs/epics/E02-identity-access/wireframes/login-unverified.excalidraw](../../E02-identity-access/wireframes/login-unverified.excalidraw) · [preview](../../E02-identity-access/wireframes/login-unverified.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/workspace-provisioning.excalidraw](../wireframes/workspace-provisioning.excalidraw) · [preview](../wireframes/workspace-provisioning.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/pricing.excalidraw](../wireframes/pricing.excalidraw) · [preview](../wireframes/pricing.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| register-org | [source](../wireframes/register-org.excalidraw) | [preview](../wireframes/register-org.svg) | +| register-org-states | [source](../wireframes/register-org-states.excalidraw) | [preview](../wireframes/register-org-states.svg) | +| email-confirmation | [source](../wireframes/email-confirmation.excalidraw) | [preview](../wireframes/email-confirmation.svg) | +| verify-email | [source](../wireframes/verify-email.excalidraw) | [preview](../wireframes/verify-email.svg) | +| verify-email-rate-limit | [source](../wireframes/verify-email-rate-limit.excalidraw) | [preview](../wireframes/verify-email-rate-limit.svg) | +| login-unverified (US-002 sign-in before verify) | [source](../../E02-identity-access/wireframes/login-unverified.excalidraw) | [preview](../../E02-identity-access/wireframes/login-unverified.svg) | + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workspace-provisioning | [source](../wireframes/workspace-provisioning.excalidraw) | [preview](../wireframes/workspace-provisioning.svg) | +| pricing | [source](../wireframes/pricing.excalidraw) | [preview](../wireframes/pricing.svg) | + --- @@ -50,10 +58,19 @@ Self-service registration flow where a new organization signs up and is automati - Social/SSO sign-up (Google, GitHub) — not in MVP. - CAPTCHA — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: none for backend US-001. `Idempotency-Key` header on `POST /api/organizations/` deduplicates rapid resubmits (Pending/Completed/Failed state). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** none for backend US-001. `Idempotency-Key` header on `POST /api/organizations/` deduplicates rapid resubmits (Pending/Completed/Failed state). > **Deferred (PR #124 follow-up):** Frontend registration confirmation-screen behavior alignment for US-001. -> Decisions: duplicate email returns silently without creating anything — matches "same confirmation screen" AC. `RegisterOrganizationCommandValidator` enforces: org name 2–100 chars, valid email, password min 8 chars + letter + number, confirmation match. Org slug auto-generated with uniqueness retry loop; BCrypt work factor 12. 4 default system roles seeded atomically in the same transaction. +> **Decisions:** duplicate email returns silently without creating anything — matches "same confirmation screen" AC. `RegisterOrganizationCommandValidator` enforces: org name 2–100 chars, valid email, password min 8 chars + letter + number, confirmation match. Org slug auto-generated with uniqueness retry loop; BCrypt work factor 12. 4 default system roles seeded atomically in the same transaction. --- @@ -81,10 +98,19 @@ Self-service registration flow where a new organization signs up and is automati *Out of scope* - Automatic re-send after X minutes — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: auto sign-in after verification click pending Frontend. **Done:** opaque one-time verification tokens in `email_verification_tokens` (SHA-256 hash at rest, 24h TTL, invalidate on verify/resend); resend rate limit 3/email/hour via `IResendVerificationRateLimiter` (Redis atomic INCR+EXPIRE per hashed email, HTTP 429 + message); login returns "Please verify your email" when unverified; `GET /api/auth/provisioning-status?token=` accepts the same link token after verify (including used tokens within TTL). IP-level `auth` limiter applies to `/connect/login` and Identity gRPC only — not on verify/resend (avoids starving integration tests). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** auto sign-in after verification click pending Frontend. **Done:** opaque one-time verification tokens in `email_verification_tokens` (SHA-256 hash at rest, 24h TTL, invalidate on verify/resend); resend rate limit 3/email/hour via `IResendVerificationRateLimiter` (Redis atomic INCR+EXPIRE per hashed email, HTTP 429 + message); login returns "Please verify your email" when unverified; `GET /api/auth/provisioning-status?token=` accepts the same link token after verify (including used tokens within TTL). IP-level `auth` limiter applies to `/connect/login` and Identity gRPC only — not on verify/resend (avoids starving integration tests). > **Deferred (PR #125 follow-up):** Frontend verify-email flow, provisioning wait screen, and post-verify auto sign-in (US-002). -> Decisions: `ResendVerificationEmailCommand` silently succeeds for unknown/already-verified emails (no info leakage). +> **Decisions:** `ResendVerificationEmailCommand` silently succeeds for unknown/already-verified emails (no info leakage). --- @@ -112,10 +138,19 @@ Self-service registration flow where a new organization signs up and is automati *Out of scope* - Custom schema naming chosen by the user — schema names are auto-generated. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⚠️ -> Gaps vs spec: provisioning wait UI (US-002) pending Frontend. **Done:** org enters `Provisioning` on verify; per-module `TenantModuleProvisionReportEvent` + Identity coordinator schedules up to 3 retries with exponential backoff; critical log alert when exhausted; `GET /api/auth/provisioning-status?token=` for polling. -> **Deferred (PR follow-up):** external paging integration for platform alerts (critical log is the MVP signal). -> Decisions: provisioning is fully event-driven over Kafka per [ADR-019](../../../TECH_STACK.md#adr-019-avro-and-schema-registry-for-event-payloads-with-cloudevents-envelope) — no central provisioner. The verify endpoint stays fast, the provisioning failure mode is decoupled from email verification, and each module owns its own schema lifecycle (satisfies ADR-010 "extraction is a redeploy"). Tenant schema name is derived from `Organization.Id` as `tenant_{orgId:N}` (32-char hex, no dashes) — stable across the lifetime of the org and safe as a Postgres identifier. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⚠️ | +> | Frontend | ⚠️ | +> +> **Gaps vs spec:** provisioning wait UI (US-002) pending Frontend. **Done:** org enters `Provisioning` on verify; per-module `TenantModuleProvisionReportEvent` + Identity coordinator schedules up to 3 retries with exponential backoff; critical log alert when exhausted; `GET /api/auth/provisioning-status?token=` for polling. +> **Deferred (PR #N follow-up):** external paging integration for platform alerts (critical log is the MVP signal). +> **Decisions:** provisioning is fully event-driven over Kafka per [ADR-019](../../../TECH_STACK.md#adr-019-avro-and-schema-registry-for-event-payloads-with-cloudevents-envelope) — no central provisioner. The verify endpoint stays fast, the provisioning failure mode is decoupled from email verification, and each module owns its own schema lifecycle (satisfies ADR-010 "extraction is a redeploy"). Tenant schema name is derived from `Organization.Id` as `tenant_{orgId:N}` (32-char hex, no dashes) — stable across the lifetime of the org and safe as a Postgres identifier. --- @@ -142,6 +177,15 @@ Self-service registration flow where a new organization signs up and is automati - Credit card collection and payment processing — Phase 2. - Plan upgrade/downgrade self-service — covered in F04. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: pricing comparison table and workspace header plan name pending Frontend. **Done (backend):** `POST /api/organizations/` accepts optional `subscriptionPlanId`; invalid/unavailable plan ids fall back to Free; org stores `subscription_plan_id`; F04 enforces limits (402) after provisioning. -> Decisions: MVP paid plan selection has no billing flag column yet — treat as normal plan assignment until billing Phase 2. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** pricing comparison table and workspace header plan name pending Frontend. **Done (backend):** `POST /api/organizations/` accepts optional `subscriptionPlanId`; invalid/unavailable plan ids fall back to Free; org stores `subscription_plan_id`; F04 enforces limits (402) after provisioning. +> **Decisions:** MVP paid plan selection has no billing flag column yet — treat as normal plan assignment until billing Phase 2. diff --git a/docs/epics/E01-platform-foundation/features/F02-organization-management.md b/docs/epics/E01-platform-foundation/features/F02-organization-management.md index 2dbb5b34..85b9711f 100644 --- a/docs/epics/E01-platform-foundation/features/F02-organization-management.md +++ b/docs/epics/E01-platform-foundation/features/F02-organization-management.md @@ -2,15 +2,20 @@ [← Back to E01](../README.md) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org.excalidraw](../wireframes/settings-org.excalidraw) · [preview](../wireframes/settings-org.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-upload-states.excalidraw](../wireframes/settings-org-upload-states.excalidraw) · [preview](../wireframes/settings-org-upload-states.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-profile-states.excalidraw](../wireframes/settings-org-profile-states.excalidraw) · [preview](../wireframes/settings-org-profile-states.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-usage-error.excalidraw](../wireframes/settings-org-usage-error.excalidraw) · [preview](../wireframes/settings-org-usage-error.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-free-plan.excalidraw](../wireframes/settings-org-free-plan.excalidraw) · [preview](../wireframes/settings-org-free-plan.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-access-denied.excalidraw](../wireframes/settings-org-access-denied.excalidraw) · [preview](../wireframes/settings-org-access-denied.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-deletion-scheduled.excalidraw](../wireframes/settings-org-deletion-scheduled.excalidraw) · [preview](../wireframes/settings-org-deletion-scheduled.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-delete-modal.excalidraw](../wireframes/settings-org-delete-modal.excalidraw) · [preview](../wireframes/settings-org-delete-modal.svg) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/settings-org-delete-states.excalidraw](../wireframes/settings-org-delete-states.excalidraw) · [preview](../wireframes/settings-org-delete-states.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| settings-org | [source](../wireframes/settings-org.excalidraw) | [preview](../wireframes/settings-org.svg) | +| settings-org-upload-states | [source](../wireframes/settings-org-upload-states.excalidraw) | [preview](../wireframes/settings-org-upload-states.svg) | +| settings-org-profile-states | [source](../wireframes/settings-org-profile-states.excalidraw) | [preview](../wireframes/settings-org-profile-states.svg) | +| settings-org-usage-error | [source](../wireframes/settings-org-usage-error.excalidraw) | [preview](../wireframes/settings-org-usage-error.svg) | +| settings-org-free-plan | [source](../wireframes/settings-org-free-plan.excalidraw) | [preview](../wireframes/settings-org-free-plan.svg) | +| settings-org-access-denied | [source](../wireframes/settings-org-access-denied.excalidraw) | [preview](../wireframes/settings-org-access-denied.svg) | +| settings-org-deletion-scheduled | [source](../wireframes/settings-org-deletion-scheduled.excalidraw) | [preview](../wireframes/settings-org-deletion-scheduled.svg) | +| settings-org-delete-modal | [source](../wireframes/settings-org-delete-modal.excalidraw) | [preview](../wireframes/settings-org-delete-modal.svg) | +| settings-org-delete-states | [source](../wireframes/settings-org-delete-states.excalidraw) | [preview](../wireframes/settings-org-delete-states.svg) | + --- @@ -48,8 +53,17 @@ Allow organization admins to manage their organization's profile, settings, and - Custom domain / vanity URL — not in MVP. - White-label theming (custom colors, fonts) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: Frontend-only AC: toast, upload progress, navigate-away warning. **Done (backend):** language tag validation (`en`, `en-US`). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Frontend-only AC: toast, upload progress, navigate-away warning. **Done (backend):** language tag validation (`en`, `en-US`). > **Done:** `Organization` profile fields + `UpdateOrganizationProfileCommand`; S3 logo storage; IANA timezone validation. --- @@ -74,8 +88,17 @@ Allow organization admins to manage their organization's profile, settings, and *Out of scope* - Editing all settings inline on this page — this page is read-only for stats; editing is in sub-sections. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: Frontend-only: usage retry UI, redirect on 403. **Done (backend):** Redis usage cache TTL ≤ 5 minutes (`PlanLimitRedisCache.UsageStatsMaxStaleness`); existing Admin roles backfilled via `OrganizationSettingsPermissionSeeder`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Frontend-only: usage retry UI, redirect on 403. **Done (backend):** Redis usage cache TTL ≤ 5 minutes (`PlanLimitRedisCache.UsageStatsMaxStaleness`); existing Admin roles backfilled via `OrganizationSettingsPermissionSeeder`. > **Done:** `GET /api/organizations/current/settings` returns plan name, profile, usage limits, deletion schedule metadata. --- @@ -107,6 +130,16 @@ Allow organization admins to manage their organization's profile, settings, and - Data export before deletion — available separately as a future feature. - Immediate hard delete without grace period — the 30-day window is non-negotiable in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: **Deferred (PR #127 follow-up):** marketing-page redirect + forced sign-out after schedule (Frontend/session); abandon in-flight Wolverine step dispatch beyond execution + form-task cancel; cross-module hard-delete steps via RabbitMQ commands when modules are extracted (see `docs/WORKAROUNDS.md#org-hard-delete-modulith-cancellers`). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** marketing-page redirect + forced sign-out after schedule (Frontend/session); abandon in-flight Wolverine step dispatch beyond execution + form-task cancel; cross-module hard-delete steps via RabbitMQ commands when modules are extracted (see `docs/WORKAROUNDS.md#org-hard-delete-modulith-cancellers`). +> **Deferred (PR #127 follow-up):** marketing-page redirect + forced sign-out after schedule (Frontend/session); abandon in-flight Wolverine step dispatch beyond execution + form-task cancel; cross-module hard-delete steps via RabbitMQ commands when modules are extracted (see `docs/WORKAROUNDS.md#org-hard-delete-modulith-cancellers`). > **Done:** schedule rollback when job queue fails; hard-delete cancels executions + pending form tasks, drops tenant schemas, deletes logo S3 object, purges Identity platform rows (users, roles, invitations, provisioning); login returns org-not-found when org row removed. diff --git a/docs/epics/E01-platform-foundation/features/F03-tenant-isolation.md b/docs/epics/E01-platform-foundation/features/F03-tenant-isolation.md index a0e7394b..d97cd495 100644 --- a/docs/epics/E01-platform-foundation/features/F03-tenant-isolation.md +++ b/docs/epics/E01-platform-foundation/features/F03-tenant-isolation.md @@ -36,9 +36,18 @@ Infrastructure-level enforcement ensuring every database query is scoped to the *Out of scope* - Cross-tenant data sharing features — not in MVP. -> **Implementation status** — Domain: N/A | Application: ✅ (`ITenantContext`) | Infrastructure: ⚠️ | API: ⚠️ | Frontend: N/A +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | N/A | +> | Application | ✅ (`ITenantContext`) | +> | Infrastructure | ⚠️ | +> | API | ⚠️ | +> | Frontend | N/A | +> > **Done:** `TenantSchemaInterceptor` sets PostgreSQL `search_path` per connection for module `DbContext`s; `TenantSchemaInterceptorTests` (two schemas, no cross-read). `HttpTenantContext` on `Axis.Api`; `FixedTenantContext` in Wolverine provision handlers. -> Gaps vs spec: no module integration test proving Tenant A records invisible to Tenant B end-to-end; no explicit guard that tenant-scoped queries cannot target `public` for module data; pool `search_path` reset not load-tested. +> **Gaps vs spec:** no module integration test proving Tenant A records invisible to Tenant B end-to-end; no explicit guard that tenant-scoped queries cannot target `public` for module data; pool `search_path` reset not load-tested. > **Next:** add cross-tenant API integration test in one module (e.g. DataModeling); document connection-pool behavior in [patterns.md](../../../playbooks/patterns.md) if verified. --- @@ -68,7 +77,16 @@ Infrastructure-level enforcement ensuring every database query is scoped to the *Out of scope* - API key authentication (alternative to JWT) — not in MVP. -> **Implementation status** — Domain: N/A | Application: ✅ | Infrastructure: ⚠️ | API: ⚠️ | Frontend: N/A +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | N/A | +> | Application | ✅ | +> | Infrastructure | ⚠️ | +> | API | ⚠️ | +> | Frontend | N/A | +> > **Done:** `org_id` claim issued at login (`ConnectEndpoints`); handlers use `ITenantContext` / `ICurrentUser` — schema `tenant_{orgId:N}` derived in `HttpTenantContext` (no separate DB lookup). -> Gaps vs spec: no Redis cache for schema name (spec allows cache miss → DB; MVP derives deterministically so cache is optional); deleted/suspended org → 403 on tenant routes not centrally enforced; Wolverine tenant header propagation not documented in one place; background jobs rely on `FixedTenantContext` in known handlers only. +> **Gaps vs spec:** no Redis cache for schema name (spec allows cache miss → DB; MVP derives deterministically so cache is optional); deleted/suspended org → 403 on tenant routes not centrally enforced; Wolverine tenant header propagation not documented in one place; background jobs rely on `FixedTenantContext` in known handlers only. > **Next:** middleware or filter rejecting JWT for `OrganizationStatus` not Active/Provisioning-complete; audit all Wolverine handlers for tenant context injection. diff --git a/docs/epics/E01-platform-foundation/features/F04-subscription-plans.md b/docs/epics/E01-platform-foundation/features/F04-subscription-plans.md index e774e7ef..b4228dea 100644 --- a/docs/epics/E01-platform-foundation/features/F04-subscription-plans.md +++ b/docs/epics/E01-platform-foundation/features/F04-subscription-plans.md @@ -2,7 +2,12 @@ [← Back to E01](../README.md) -> **Wireframe**: [docs/epics/E01-platform-foundation/wireframes/pricing.excalidraw](../wireframes/pricing.excalidraw) · [preview](../wireframes/pricing.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| pricing | [source](../wireframes/pricing.excalidraw) | [preview](../wireframes/pricing.svg) | + --- @@ -85,8 +90,17 @@ Define subscription plan tiers with feature limits and enforce those limits at t *Out of scope* - Org-initiated plan upgrade — Phase 2 (requires billing integration). -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ (backend AC) | Frontend: ⏳ -> Gaps vs spec: US-010 pricing page UI, static fallback on load failure, "Current plan" badge (Frontend only). No multi-workflow bulk-import API yet — single-workflow import/duplicate enforce +1 before save (US-011 bulk AC applies when bulk endpoint exists). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ (backend AC) | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** US-010 pricing page UI, static fallback on load failure, "Current plan" badge (Frontend only). No multi-workflow bulk-import API yet — single-workflow import/duplicate enforce +1 before save (US-011 bulk AC applies when bulk endpoint exists). > **Done (backend):** `GET /api/plans` with limits + `featureFlags` + `isAvailableForNewSignups`; signed-in org on retired plan still sees that plan; 402 on create/duplicate/import workflow, invite user, start execution; Redis read-through cache + INCR/DECR on mutate + execution key TTL to month-end UTC; DB fallback + warning when Redis write/read fails; delete workflow decrements cache; platform plan change 403 + audit log + cache refresh; downgrade over limit blocks new creates via existing usage check (resources not deleted). -> **Deferred (follow-up PR):** atomic check-and-consume for monthly execution starts (race can briefly exceed cap under concurrency); fail-closed when usage counter/Redis unavailable (today logs warning and treats usage as 0). -> Decisions: `featureFlags` derived from plan slug for MVP (no JSON column); `PlatformAdmin:UserIds` config for US-012. +> **Deferred (PR #N follow-up):** atomic check-and-consume for monthly execution starts (race can briefly exceed cap under concurrency); fail-closed when usage counter/Redis unavailable (today logs warning and treats usage as 0). +> **Decisions:** `featureFlags` derived from plan slug for MVP (no JSON column); `PlatformAdmin:UserIds` config for US-012. diff --git a/docs/epics/E02-identity-access/features/F01-authentication.md b/docs/epics/E02-identity-access/features/F01-authentication.md index d2532e84..a23fd278 100644 --- a/docs/epics/E02-identity-access/features/F01-authentication.md +++ b/docs/epics/E02-identity-access/features/F01-authentication.md @@ -1,8 +1,13 @@ # F01 — Authentication -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/login.excalidraw](../wireframes/login.excalidraw) · [preview](../wireframes/login.svg) -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/login-unverified.excalidraw](../wireframes/login-unverified.excalidraw) · [preview](../wireframes/login-unverified.svg) -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/register.excalidraw](../wireframes/register.excalidraw) · [preview](../wireframes/register.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| login | [source](../wireframes/login.excalidraw) | [preview](../wireframes/login.svg) | +| login-unverified | [source](../wireframes/login-unverified.excalidraw) | [preview](../wireframes/login-unverified.svg) | +| register | [source](../wireframes/register.excalidraw) | [preview](../wireframes/register.svg) | + [← Back to E02](../README.md) @@ -45,10 +50,19 @@ Secure sign-in and sign-out flows using JWT access tokens and opaque refresh tok - SSO / social login (Google, GitHub) — not in MVP. - 2FA / MFA — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⚠️ -> Gaps vs spec: Login page + PKCE flow + app shell/dashboard scaffold on PR #50 branch. BroadcastChannel multi-tab refresh, account lockout UI, and unverified-email screen polish pending. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⚠️ | +> +> **Gaps vs spec:** Login page + PKCE flow + app shell/dashboard scaffold on PR #50 branch. BroadcastChannel multi-tab refresh, account lockout UI, and unverified-email screen polish pending. > **Deferred (PR #50 follow-up):** none for this US. -> Decisions: OpenIddict 5.x serves as the in-process OAuth2/OIDC server. `AuthenticateUserCommand` validates credentials; `/connect/login` sets a 5-min httpOnly session cookie; `/connect/authorize` issues the authorization code; `/connect/token` exchanges it for access + refresh tokens. Refresh token stored as an opaque reference in DB (OpenIddict `OpenIddictTokens` table) and delivered as an httpOnly `Secure SameSite=Strict` cookie at `/connect` path via `ApplyRefreshTokenCookieHandler`. +> **Decisions:** OpenIddict 5.x serves as the in-process OAuth2/OIDC server. `AuthenticateUserCommand` validates credentials; `/connect/login` sets a 5-min httpOnly session cookie; `/connect/authorize` issues the authorization code; `/connect/token` exchanges it for access + refresh tokens. Refresh token stored as an opaque reference in DB (OpenIddict `OpenIddictTokens` table) and delivered as an httpOnly `Secure SameSite=Strict` cookie at `/connect` path via `ApplyRefreshTokenCookieHandler`. --- @@ -75,9 +89,18 @@ Secure sign-in and sign-out flows using JWT access tokens and opaque refresh tok *Out of scope* - Server-side session management (stateful sessions) — access is stateless JWT-based. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: BroadcastChannel multi-tab coordination is Frontend-only. Endpoint is `POST /connect/token` (grant_type=refresh_token) instead of `/api/auth/refresh`. -> Decisions: OpenIddict handles token rotation natively. `ExtractRefreshTokenFromCookieHandler` reads the refresh token from the httpOnly cookie into the OpenIddict request. `POST /connect/token` with grant_type=refresh_token validates the opaque reference token, loads fresh user+permissions, rotates the refresh token, returns a new access token in the JSON body and a new refresh token cookie. Replay detection: reference tokens are single-use; replaying revoked token returns `invalid_grant`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** BroadcastChannel multi-tab coordination is Frontend-only. Endpoint is `POST /connect/token` (grant_type=refresh_token) instead of `/api/auth/refresh`. +> **Decisions:** OpenIddict handles token rotation natively. `ExtractRefreshTokenFromCookieHandler` reads the refresh token from the httpOnly cookie into the OpenIddict request. `POST /connect/token` with grant_type=refresh_token validates the opaque reference token, loads fresh user+permissions, rotates the refresh token, returns a new access token in the JSON body and a new refresh token cookie. Replay detection: reference tokens are single-use; replaying revoked token returns `invalid_grant`. --- @@ -104,5 +127,14 @@ Secure sign-in and sign-out flows using JWT access tokens and opaque refresh tok *Out of scope* - "Sign out of all devices" from this flow — covered in [F05 Password & Security](./F05-password-security.md). -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Decisions: `POST /api/auth/signout` [Authorize] reads the opaque refresh token from the httpOnly cookie, calls `IOpenIddictTokenManager.FindByReferenceIdAsync` + `TryRevokeAsync` to revoke it in DB, blacklists the access token JTI in Redis via `IJtiBlacklist` (TTL = remaining access token lifetime), clears the refresh token cookie and the PKCE session cookie. No Application handler — pure API/Infrastructure concern. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Decisions:** `POST /api/auth/signout` [Authorize] reads the opaque refresh token from the httpOnly cookie, calls `IOpenIddictTokenManager.FindByReferenceIdAsync` + `TryRevokeAsync` to revoke it in DB, blacklists the access token JTI in Redis via `IJtiBlacklist` (TTL = remaining access token lifetime), clears the refresh token cookie and the PKCE session cookie. No Application handler — pure API/Infrastructure concern. diff --git a/docs/epics/E02-identity-access/features/F02-user-management.md b/docs/epics/E02-identity-access/features/F02-user-management.md index 316158df..9d345ec5 100644 --- a/docs/epics/E02-identity-access/features/F02-user-management.md +++ b/docs/epics/E02-identity-access/features/F02-user-management.md @@ -1,7 +1,12 @@ # F02 — User Management -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/settings-users.excalidraw](../wireframes/settings-users.excalidraw) · [preview](../wireframes/settings-users.svg) -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/accept-invitation.excalidraw](../wireframes/accept-invitation.excalidraw) · [preview](../wireframes/accept-invitation.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| settings-users | [source](../wireframes/settings-users.excalidraw) | [preview](../wireframes/settings-users.svg) | +| accept-invitation | [source](../wireframes/accept-invitation.excalidraw) | [preview](../wireframes/accept-invitation.svg) | + [← Back to E02](../README.md) @@ -42,9 +47,18 @@ Organization admins can invite new members, manage their accounts, and deactivat *Out of scope* - Bulk invitation via CSV upload — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: **Done:** HTTP 402 when user plan limit reached (`InviteUserHandler`, E01 F04). Admin self-invite check not implemented — pending API layer (compare invite email to `ICurrentUser` email). -> Decisions: existing-member and pending-invitation checks throw `ValidationException` with specific messages matching AC wording. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** **Done:** HTTP 402 when user plan limit reached (`InviteUserHandler`, E01 F04). Admin self-invite check not implemented — backend polish — see gaps below (compare invite email to `ICurrentUser` email). +> **Decisions:** existing-member and pending-invitation checks throw `ValidationException` with specific messages matching AC wording. --- @@ -72,9 +86,18 @@ Organization admins can invite new members, manage their accounts, and deactivat *Out of scope* - Inviting users who already have accounts on other orgs to join a second org simultaneously — each user belongs to one org in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: session sign-in after accept is an API/auth concern, pending. -> Decisions: expired/accepted/cancelled invitation states enforced in `Invitation.Accept()` domain method, wrapped as `ValidationException` in handler. Platform-wide email check runs after invitation validation — throws `ValidationException` directing user to sign in with existing credentials. `AcceptInvitationHandler` calls `user.VerifyEmail()` — the invitation link proves mailbox ownership (no separate verification email). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** session sign-in after accept is an API/auth concern, pending. +> **Decisions:** expired/accepted/cancelled invitation states enforced in `Invitation.Accept()` domain method, wrapped as `ValidationException` in handler. Platform-wide email check runs after invitation validation — throws `ValidationException` directing user to sign in with existing credentials. `AcceptInvitationHandler` calls `user.VerifyEmail()` — the invitation link proves mailbox ownership (no separate verification email). --- @@ -103,9 +126,18 @@ Organization admins can invite new members, manage their accounts, and deactivat *Out of scope* - Transferring ownership of content from a deactivated user — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: session revocation (refresh token revoke + access token blacklist) not implemented — pending auth infrastructure (OpenIddict + Redis). Self-deactivation guard and 403 check require current user identity from JWT — pending API layer. Deactivated-user sign-in message handled at auth layer (pending). -> Decisions: "last admin" check queries `CountAdminsAsync` in the repository before deactivating — domain enforces via `ApplicationException` if violated. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** session revocation (refresh token revoke + access token blacklist) not implemented — auth infrastructure polish — see gaps below. Self-deactivation guard and 403 check require current user identity from JWT — backend polish — see gaps below. Deactivated-user sign-in message handled at auth layer (pending). +> **Decisions:** "last admin" check queries `CountAdminsAsync` in the repository before deactivating — domain enforces via `ApplicationException` if violated. --- @@ -131,6 +163,15 @@ Organization admins can invite new members, manage their accounts, and deactivat *Out of scope* - Public profile visibility — all profiles are private within the org in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: email change flow (F05) not started. -> Decisions: name update is a direct property mutation on `User` aggregate with a `UserProfileUpdatedEvent`. Avatar upload fully wired in `UpdateUserProfileHandler` — validates type (PNG/JPG only) and size (max 1 MB), uploads to S3, deletes old file on replacement. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** email change flow (F05) not started. +> **Decisions:** name update is a direct property mutation on `User` aggregate with a `UserProfileUpdatedEvent`. Avatar upload fully wired in `UpdateUserProfileHandler` — validates type (PNG/JPG only) and size (max 1 MB), uploads to S3, deletes old file on replacement. diff --git a/docs/epics/E02-identity-access/features/F03-role-management.md b/docs/epics/E02-identity-access/features/F03-role-management.md index b287b1ec..690b63d8 100644 --- a/docs/epics/E02-identity-access/features/F03-role-management.md +++ b/docs/epics/E02-identity-access/features/F03-role-management.md @@ -1,6 +1,11 @@ # F03 — Role Management -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/settings-roles.excalidraw](../wireframes/settings-roles.excalidraw) · [preview](../wireframes/settings-roles.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| settings-roles | [source](../wireframes/settings-roles.excalidraw) | [preview](../wireframes/settings-roles.svg) | + [← Back to E02](../README.md) @@ -34,8 +39,17 @@ Organization admins can create custom roles, assign permissions to each role, an *Out of scope* - Role hierarchy / role inheritance — not in MVP (flat role model only). -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: member count per role requires a JOIN query — not implemented yet (pending API query layer). UI badges and detail view are frontend concerns. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** member count per role requires a JOIN query — not implemented yet (query projection polish — see gaps below). UI badges and detail view are frontend concerns. --- @@ -62,9 +76,18 @@ Organization admins can create custom roles, assign permissions to each role, an *Out of scope* - Copying permissions from an existing role as a starting point — not in MVP (user selects permissions manually). -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: 403 permission check requires JWT identity from API layer — pending. Case-insensitive name uniqueness check is done in handler against existing roles in org. -> Decisions: `Role.CreateCustom(name, orgId, permissions[])` factory method; minimum 1 permission enforced in domain. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** 403 permission check requires JWT identity from API layer — pending. Case-insensitive name uniqueness check is done in handler against existing roles in org. +> **Decisions:** `Role.CreateCustom(name, orgId, permissions[])` factory method; minimum 1 permission enforced in domain. --- @@ -91,8 +114,17 @@ Organization admins can create custom roles, assign permissions to each role, an *Out of scope* - Permission change notifications to affected users — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: system role guard (cannot edit Admin/Editor/Viewer/End User) implemented in domain via `IsSystem` flag. 403 check pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** system role guard (cannot edit Admin/Editor/Viewer/End User) implemented in domain via `IsSystem` flag. 403 check backend polish — see gaps below. --- @@ -118,6 +150,15 @@ Organization admins can create custom roles, assign permissions to each role, an *Out of scope* - Time-limited role assignments (e.g., "grant admin for 24 hours") — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: 403 check pending API layer. "At least one role" guard and "last admin" guard both implemented in handler. -> Decisions: roles stored as `List` (`_roleIds`) on `User` aggregate — effective permissions are the union of all assigned roles' permission lists, computed at token issuance time (pending auth layer). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** 403 check backend polish — see gaps below. "At least one role" guard and "last admin" guard both implemented in handler. +> **Decisions:** roles stored as `List` (`_roleIds`) on `User` aggregate — effective permissions are the union of all assigned roles' permission lists, computed at token issuance time (pending auth layer). diff --git a/docs/epics/E02-identity-access/features/F04-permissions.md b/docs/epics/E02-identity-access/features/F04-permissions.md index 6f3f97fc..f26ffa72 100644 --- a/docs/epics/E02-identity-access/features/F04-permissions.md +++ b/docs/epics/E02-identity-access/features/F04-permissions.md @@ -1,6 +1,11 @@ # F04 — Permission System -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/settings-roles.excalidraw](../wireframes/settings-roles.excalidraw) · [preview](../wireframes/settings-roles.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| settings-roles | [source](../wireframes/settings-roles.excalidraw) | [preview](../wireframes/settings-roles.svg) | + [← Back to E02](../README.md) @@ -83,9 +88,18 @@ A resource-based permission system where each permission grants the ability to p *Out of scope* - Row-level security (e.g., "user can only edit their own records") — not in MVP; all permission checks are type-level. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: policy-based authorization middleware, `[RequirePermission]` attribute, and automated permission tests pending API layer. -> Decisions: permissions are included as a flat array in JWT claims at sign-in time (union of all role permissions); checked via ASP.NET Core custom policy at API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** policy-based authorization middleware, `[RequirePermission]` attribute, and automated permission tests backend polish — see gaps below. +> **Decisions:** permissions are included as a flat array in JWT claims at sign-in time (union of all role permissions); checked via ASP.NET Core custom policy at API layer. --- @@ -111,5 +125,14 @@ A resource-based permission system where each permission grants the ability to p *Out of scope* - Per-record UI permissions (e.g., hiding individual table rows) — not in MVP. -> **Implementation status** — Domain + Application: ⏳ | Infrastructure: ⏳ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: all ACs are frontend + API concerns — no Application-layer handler needed. Pending API layer for policy middleware and Frontend for UI hiding logic. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ⏳ | +> | Application | ⏳ | +> | Infrastructure | ⏳ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** all ACs are frontend + API concerns — no Application-layer handler needed. Pending API layer for policy middleware and Frontend for UI hiding logic. diff --git a/docs/epics/E02-identity-access/features/F05-password-security.md b/docs/epics/E02-identity-access/features/F05-password-security.md index d56361a8..be3b5dd5 100644 --- a/docs/epics/E02-identity-access/features/F05-password-security.md +++ b/docs/epics/E02-identity-access/features/F05-password-security.md @@ -1,8 +1,13 @@ # F05 — Password & Security Management -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/settings-security.excalidraw](../wireframes/settings-security.excalidraw) · [preview](../wireframes/settings-security.svg) -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/forgot-password.excalidraw](../wireframes/forgot-password.excalidraw) · [preview](../wireframes/forgot-password.svg) -> **Wireframe**: [docs/epics/E02-identity-access/wireframes/change-password.excalidraw](../wireframes/change-password.excalidraw) · [preview](../wireframes/change-password.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| settings-security | [source](../wireframes/settings-security.excalidraw) | [preview](../wireframes/settings-security.svg) | +| forgot-password | [source](../wireframes/forgot-password.excalidraw) | [preview](../wireframes/forgot-password.svg) | +| change-password | [source](../wireframes/change-password.excalidraw) | [preview](../wireframes/change-password.svg) | + [← Back to E02](../README.md) @@ -45,9 +50,18 @@ Allow users to reset forgotten passwords, change their current password, and man *Out of scope* - Security questions as a backup reset method — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: rate limiting (max 3 requests/hour) is an API/Infrastructure concern — pending API layer. Auto sign-in after reset pending API layer. -> Decisions: reset token is a cryptographically random 32-byte value; stored as SHA-256 hash in `password_reset_tokens` table; raw token sent by email. New request invalidates all prior tokens for the user. Token lifetime: 1 hour. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** rate limiting (max 3 requests/hour) is an API/Infrastructure concern — backend polish — see gaps below. Auto sign-in after reset backend polish — see gaps below. +> **Decisions:** reset token is a cryptographically random 32-byte value; stored as SHA-256 hash in `password_reset_tokens` table; raw token sent by email. New request invalidates all prior tokens for the user. Token lifetime: 1 hour. --- @@ -75,9 +89,18 @@ Allow users to reset forgotten passwords, change their current password, and man *Out of scope* - Password history check (cannot reuse last N passwords) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: failed-attempt lockout for change-password form (3 attempts / 15 min) pending API layer. Revoking other-device sessions after change pending API layer (OpenIddict token revocation). -> Decisions: notification email failure is swallowed at handler level and logged separately at Infrastructure — password change still succeeds per US-028 AC. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** failed-attempt lockout for change-password form (3 attempts / 15 min) backend polish — see gaps below. Revoking other-device sessions after change backend polish — see gaps below (OpenIddict token revocation). +> **Decisions:** notification email failure is swallowed at handler level and logged separately at Infrastructure — password change still succeeds per US-028 AC. --- @@ -105,6 +128,15 @@ Allow users to reset forgotten passwords, change their current password, and man *Out of scope* - Per-session device naming by the user — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: `ISessionStore` interface defined in Application; implementation pending Infrastructure (OpenIddict token manager wrapper). Session list UI, revoke button, and "sign out everywhere" pending Frontend + API. -> Decisions: sessions are modelled as OpenIddict refresh tokens; `ISessionStore` wraps `IOpenIddictTokenManager` at Infrastructure layer. `RevokeSessionCommand(sessionId: null)` triggers "revoke all" via `RevokeAllAsync`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `ISessionStore` interface defined in Application; implementation pending Infrastructure (OpenIddict token manager wrapper). Session list UI, revoke button, and "sign out everywhere" pending Frontend + API. +> **Decisions:** sessions are modelled as OpenIddict refresh tokens; `ISessionStore` wraps `IOpenIddictTokenManager` at Infrastructure layer. `RevokeSessionCommand(sessionId: null)` triggers "revoke all" via `RevokeAllAsync`. diff --git a/docs/epics/E03-data-modeling/features/F01-model-definition.md b/docs/epics/E03-data-modeling/features/F01-model-definition.md index 3434ba78..dcc81b75 100644 --- a/docs/epics/E03-data-modeling/features/F01-model-definition.md +++ b/docs/epics/E03-data-modeling/features/F01-model-definition.md @@ -1,6 +1,11 @@ # F01 — Model Definition -> **Wireframe**: [docs/epics/E03-data-modeling/wireframes/data-models.excalidraw](../wireframes/data-models.excalidraw) · [preview](../wireframes/data-models.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| data-models | [source](../wireframes/data-models.excalidraw) | [preview](../wireframes/data-models.svg) | + [← Back to E03](../README.md) @@ -37,9 +42,18 @@ Users can create custom data models within their organization. A model defines t *Out of scope* - Importing a model from another org or from a JSON file directly — covered in [E04 F07 Import/Export](../../E04-workflow-builder/features/F07-import-export.md). -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: model plan-limit check (HTTP 402) pending billing layer (E01 F04); name format validation enforced in Application handler. -> Decisions: system fields (id, created_at, updated_at) injected by domain factory; atomicity guaranteed by UnitOfWork. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** model plan-limit check (HTTP 402) pending billing layer (E01 F04); name format validation enforced in Application handler. +> **Decisions:** system fields (id, created_at, updated_at) injected by domain factory; atomicity guaranteed by UnitOfWork. --- @@ -65,8 +79,17 @@ Users can create custom data models within their organization. A model defines t *Out of scope* - Folders or categories for organizing models — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: record count column pending denormalized counter or API-layer aggregation; field count is derived from Fields.Count at query time. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** record count column pending denormalized counter or API-layer aggregation; field count is derived from Fields.Count at query time. --- @@ -94,8 +117,17 @@ Users can create custom data models within their organization. A model defines t *Out of scope* - Undo history for field changes — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: HTTP 409 version-conflict check pending API layer (updated_at comparison); active-workflow warning pending E04 integration. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** HTTP 409 version-conflict check backend polish — see gaps below (updated_at comparison); active-workflow warning pending E04 integration. --- @@ -122,6 +154,15 @@ Users can create custom data models within their organization. A model defines t *Out of scope* - Recovering a soft-deleted model — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: workflow reference check pending E04; form Relation Picker refs blocked/flagged via FormBuilder `ModelDeletedEvent` consumer (US-033 partial); 30-day purge background job pending. -> **Deferred:** DataModeling relation fields on other models flagged broken when target model deleted. WorkflowBuilder `record.*` trigger broken flags shipped via `ModelDeletedHandler` (Kafka). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** workflow reference check pending E04; form Relation Picker refs blocked/flagged via FormBuilder `ModelDeletedEvent` consumer (US-033 partial); 30-day purge background job pending. +> **Deferred (PR #N follow-up):** DataModeling relation fields on other models flagged broken when target model deleted. WorkflowBuilder `record.*` trigger broken flags shipped via `ModelDeletedHandler` (Kafka). diff --git a/docs/epics/E03-data-modeling/features/F02-field-types.md b/docs/epics/E03-data-modeling/features/F02-field-types.md index 3c34fc50..5f48da4b 100644 --- a/docs/epics/E03-data-modeling/features/F02-field-types.md +++ b/docs/epics/E03-data-modeling/features/F02-field-types.md @@ -1,6 +1,11 @@ # F02 — Field Type System -> **Wireframe**: [docs/epics/E03-data-modeling/wireframes/data-models.excalidraw](../wireframes/data-models.excalidraw) · [preview](../wireframes/data-models.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| data-models | [source](../wireframes/data-models.excalidraw) | [preview](../wireframes/data-models.svg) | + [← Back to E03](../README.md) @@ -57,8 +62,17 @@ Each field in a model has a type that determines what data it stores, how it's v *Out of scope* - Computed / formula fields (e.g., "full_name = first_name + last_name") — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Decisions: all 9 field types serialized to JSONB via custom `FieldDefinitionConverter` — polymorphic FieldConfig deserialized using the `type` discriminator in the JSON object. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Decisions:** all 9 field types serialized to JSONB via custom `FieldDefinitionConverter` — polymorphic FieldConfig deserialized using the `type` discriminator in the JSON object. --- @@ -86,8 +100,17 @@ Each field in a model has a type that determines what data it stores, how it's v *Out of scope* - Cross-field validation (e.g., "end_date must be after start_date") — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: server-side per-field validation on record create/update is in Application handlers but HTTP 422 structured errors pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** server-side per-field validation on record create/update is in Application handlers but HTTP 422 structured errors backend polish — see gaps below. --- @@ -112,5 +135,14 @@ Each field in a model has a type that determines what data it stores, how it's v *Out of scope* - Hiding fields from the default list view per user — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: drag-drop reorder UX and immediate-save endpoint pending Frontend layer; `displayOrder` persisted in JSONB field list. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** drag-drop reorder UX and immediate-save endpoint pending Frontend layer; `displayOrder` persisted in JSONB field list. diff --git a/docs/epics/E03-data-modeling/features/F03-data-classes.md b/docs/epics/E03-data-modeling/features/F03-data-classes.md index a32ce073..1824a3b1 100644 --- a/docs/epics/E03-data-modeling/features/F03-data-classes.md +++ b/docs/epics/E03-data-modeling/features/F03-data-classes.md @@ -1,6 +1,11 @@ # F03 — Data Class Management -> **Wireframe**: [docs/epics/E03-data-modeling/wireframes/data-classes.excalidraw](../wireframes/data-classes.excalidraw) · [preview](../wireframes/data-classes.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| data-classes | [source](../wireframes/data-classes.excalidraw) | [preview](../wireframes/data-classes.svg) | + [← Back to E03](../README.md) @@ -35,8 +40,17 @@ Data Classes are reusable, named object types composed of multiple fields. They *Out of scope* - Nested data classes (data class within a data class) — depth limited to 1 in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Decisions: DataClass fields stored as JSONB using the same FieldDefinitionConverter as DataModel; Relation/DataClass/File types blocked in domain by guard. DataClassDefinition reuses `FieldDefinition` directly — no separate DataClassField entity. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Decisions:** DataClass fields stored as JSONB using the same FieldDefinitionConverter as DataModel; Relation/DataClass/File types blocked in domain by guard. DataClassDefinition reuses `FieldDefinition` directly — no separate DataClassField entity. --- @@ -63,8 +77,17 @@ Data Classes are reusable, named object types composed of multiple fields. They *Out of scope* - A field referencing a data class from another org — tenant-isolated, not possible. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: sub-form rendering and record-list summary display pending Frontend layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** sub-form rendering and record-list summary display pending Frontend layer. --- @@ -88,8 +111,17 @@ Data Classes are reusable, named object types composed of multiple fields. They *Out of scope* - Version history of data class changes — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: "models affected" warning on field delete pending API/Frontend layer; auto-downgrade-to-optional for required fields on existing-record models pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** "models affected" warning on field delete pending API/Frontend layer; auto-downgrade-to-optional for required fields on existing-record models backend polish — see gaps below. --- @@ -113,6 +145,15 @@ Data Classes are reusable, named object types composed of multiple fields. They *Out of scope* - Merging two data classes into one — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: HTTP 409 on delete-while-referenced enforced in Application handler; reference check uses PostgreSQL JSONB `@>` containment query. -> Decisions: `IsReferencedByAnyModelAsync` uses raw SQL `fields @> {0}::jsonb` to query nested JSON without loading all models into memory. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** HTTP 409 on delete-while-referenced enforced in Application handler; reference check uses PostgreSQL JSONB `@>` containment query. +> **Decisions:** `IsReferencedByAnyModelAsync` uses raw SQL `fields @> {0}::jsonb` to query nested JSON without loading all models into memory. diff --git a/docs/epics/E03-data-modeling/features/F04-data-records.md b/docs/epics/E03-data-modeling/features/F04-data-records.md index 333c49cc..813409c9 100644 --- a/docs/epics/E03-data-modeling/features/F04-data-records.md +++ b/docs/epics/E03-data-modeling/features/F04-data-records.md @@ -1,6 +1,11 @@ # F04 — Data Record CRUD -> **Wireframe**: [docs/epics/E03-data-modeling/wireframes/records.excalidraw](../wireframes/records.excalidraw) · [preview](../wireframes/records.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| records | [source](../wireframes/records.excalidraw) | [preview](../wireframes/records.svg) | + [← Back to E03](../README.md) @@ -39,10 +44,19 @@ Users can create, read, update, and delete records against any model. Records ar *Out of scope* - Record templates (pre-filled forms) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: File field pre-upload step pending file storage service; Relation field existence check pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** File field pre-upload step pending file storage service; Relation field existence check backend polish — see gaps below. > Diagram pending: entity name `Record` → `DataRecord` in data-model diagram (`dataModelDiagram()` in `generate-diagrams.mjs`) — `Record` is a C# keyword and conflicts with the language reserved word. -> Decisions: record data stored as `Dictionary` serialized to JSONB column `_data`. +> **Decisions:** record data stored as `Dictionary` serialized to JSONB column `_data`. --- @@ -71,8 +85,17 @@ Users can create, read, update, and delete records against any model. Records ar - Saved views / custom column configurations — not in MVP. - Inline editing in the list — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: Relation display-field resolution (showing target record's display_field value instead of raw UUID) pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Relation display-field resolution (showing target record's display_field value instead of raw UUID) backend polish — see gaps below. --- @@ -100,9 +123,18 @@ Users can create, read, update, and delete records against any model. Records ar - OR-logic between filters — not in MVP. - Saved filters — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: filter-state URL persistence is a frontend concern (query params round-tripped via `?filter=field:op:value`); filter on a deleted field falls back gracefully (RecordFilter.TryParse validates field name format, unknown fields simply match no JSONB data). A filter on a deleted field currently returns 0 results rather than showing a warning toast — that warning is a frontend concern. -> Decisions: per-field filters encoded as repeated `?filter=field:op:value` query params (URL-shareable); ops supported: eq, contains, gt, lt, isEmpty, isNotEmpty; multiple filters combined with AND; sort via `?sortBy=field&sortDir=asc|desc`; unknown/unsafe field names in sort fall back to `created_at DESC`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** filter-state URL persistence is a frontend concern (query params round-tripped via `?filter=field:op:value`); filter on a deleted field falls back gracefully (RecordFilter.TryParse validates field name format, unknown fields simply match no JSONB data). A filter on a deleted field currently returns 0 results rather than showing a warning toast — that warning is a frontend concern. +> **Decisions:** per-field filters encoded as repeated `?filter=field:op:value` query params (URL-shareable); ops supported: eq, contains, gt, lt, isEmpty, isNotEmpty; multiple filters combined with AND; sort via `?sortBy=field&sortDir=asc|desc`; unknown/unsafe field names in sort fall back to `created_at DESC`. --- @@ -128,8 +160,17 @@ Users can create, read, update, and delete records against any model. Records ar *Out of scope* - Edit history / audit trail per record — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: HTTP 409 optimistic concurrency (updated_at comparison) pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** HTTP 409 optimistic concurrency (updated_at comparison) backend polish — see gaps below. --- @@ -155,8 +196,17 @@ Users can create, read, update, and delete records against any model. Records ar *Out of scope* - Restoring a soft-deleted record — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: Relation broken-reference warning pending E04 integration; 30-day purge pending background job scheduler. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Relation broken-reference warning pending E04 integration; 30-day purge pending background job scheduler. --- @@ -182,6 +232,15 @@ Users can create, read, update, and delete records against any model. Records ar *Out of scope* - Bulk edit (updating multiple records at once) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: async export for >5,000 records deferred — pending Wolverine background job + in-app notification infrastructure; current sync CSV export has no size limit (streams in 500-record chunks). "Select all N records across all pages" for bulk delete is a frontend concern. -> Decisions: bulk delete via `POST /api/models/{id}/records/bulk-delete` with `{ "ids": [...] }` body; CSV export via `GET /api/models/{id}/records/export` (same filter/sort params as list); field names for export header taken from model's FieldDefinition labels; CSV uses RFC 4180 escaping. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** async export for >5,000 records deferred — pending Wolverine background job + in-app notification infrastructure; current sync CSV export has no size limit (streams in 500-record chunks). "Select all N records across all pages" for bulk delete is a frontend concern. +> **Decisions:** bulk delete via `POST /api/models/{id}/records/bulk-delete` with `{ "ids": [...] }` body; CSV export via `GET /api/models/{id}/records/export` (same filter/sort params as list); field names for export header taken from model's FieldDefinition labels; CSV uses RFC 4180 escaping. diff --git a/docs/epics/E04-workflow-builder/features/F01-workflow-definition.md b/docs/epics/E04-workflow-builder/features/F01-workflow-definition.md index a0c823ac..cfaddb3f 100644 --- a/docs/epics/E04-workflow-builder/features/F01-workflow-definition.md +++ b/docs/epics/E04-workflow-builder/features/F01-workflow-definition.md @@ -1,6 +1,11 @@ # F01 — Workflow Definition Management -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflows.excalidraw](../wireframes/workflows.excalidraw) · [preview](../wireframes/workflows.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflows | [source](../wireframes/workflows.excalidraw) | [preview](../wireframes/workflows.svg) | + [← Back to E04](../README.md) @@ -35,9 +40,18 @@ Users can create, view, edit, publish, archive, delete, and duplicate workflow d *Out of scope* - Workflow templates / starter library — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: canvas/list UI only (backend). **Done:** HTTP 402 on create when workflow plan limit reached (`CreateWorkflowHandler` + E01 F04). -> Decisions: new workflow initialised with Start + End nodes by domain factory; all data stored in single `workflow_definitions` table. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** canvas/list UI only (backend). **Done:** HTTP 402 on create when workflow plan limit reached (`CreateWorkflowHandler` + E01 F04). +> **Decisions:** new workflow initialised with Start + End nodes by domain factory; all data stored in single `workflow_definitions` table. --- @@ -62,8 +76,17 @@ Users can create, view, edit, publish, archive, delete, and duplicate workflow d *Out of scope* - Workflow folders / tags — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: status-tab filter and last-execution-date column pending API layer; execution date requires WorkflowEngine integration. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** status-tab filter and last-execution-date column backend polish — see gaps below; execution date requires WorkflowEngine integration. --- @@ -88,8 +111,17 @@ Users can create, view, edit, publish, archive, delete, and duplicate workflow d *Out of scope* - Approval workflow for publishing (e.g., requiring a second admin to approve) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: cron job registration and webhook URL generation pending WorkflowEngine integration (E06); broken-step validation pending E03/E05 integration; draft versioning on re-edit pending API design. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** cron job registration and webhook URL generation pending WorkflowEngine integration (E06); broken-step validation pending E03/E05 integration; draft versioning on re-edit pending API design. --- @@ -114,8 +146,17 @@ Users can create, view, edit, publish, archive, delete, and duplicate workflow d *Out of scope* - Automatic archiving after N days of inactivity — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: trigger deactivation on archive pending E06 integration; HTTP 422 on archived-workflow trigger pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** trigger deactivation on archive pending E06 integration; HTTP 422 on archived-workflow trigger backend polish — see gaps below. --- @@ -141,9 +182,18 @@ Users can create, view, edit, publish, archive, delete, and duplicate workflow d *Out of scope* - Cross-org workflow duplication (copy to another org) — handled by Import/Export in F07. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: webhook URL generation for duplicate pending E06. **Done:** HTTP 402 on duplicate when at workflow limit (`DuplicateWorkflowHandler`). -> Decisions: Duplicate() deep-copies all steps with new IDs and remaps transitions atomically in domain logic; handler resolves name collisions via "(2)", "(3)"… suffix loop up to 50, then Guid suffix. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** webhook URL generation for duplicate pending E06. **Done:** HTTP 402 on duplicate when at workflow limit (`DuplicateWorkflowHandler`). +> **Decisions:** Duplicate() deep-copies all steps with new IDs and remaps transitions atomically in domain logic; handler resolves name collisions via "(2)", "(3)"… suffix loop up to 50, then Guid suffix. --- @@ -165,4 +215,13 @@ Users can create, view, edit, publish, archive, delete, and duplicate workflow d - Hard delete / permanent purge — not in MVP. - Bulk delete — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> diff --git a/docs/epics/E04-workflow-builder/features/F02-visual-canvas.md b/docs/epics/E04-workflow-builder/features/F02-visual-canvas.md index 9f7782d0..8e555c2e 100644 --- a/docs/epics/E04-workflow-builder/features/F02-visual-canvas.md +++ b/docs/epics/E04-workflow-builder/features/F02-visual-canvas.md @@ -1,6 +1,11 @@ # F02 — Visual Workflow Canvas -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflow-editor.excalidraw](../wireframes/workflow-editor.excalidraw) · [preview](../wireframes/workflow-editor.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflow-editor | [source](../wireframes/workflow-editor.excalidraw) | [preview](../wireframes/workflow-editor.svg) | + [← Back to E04](../README.md) @@ -37,8 +42,17 @@ A node-based drag-and-drop canvas (powered by React Flow) where users design the *Out of scope* - Copy-paste of steps — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: canvas drag-drop UI and 1-second debounce auto-save pending Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** canvas drag-drop UI and 1-second debounce auto-save pending Frontend. --- @@ -65,8 +79,17 @@ A node-based drag-and-drop canvas (powered by React Flow) where users design the *Out of scope* - Animated transitions showing flow direction — not in MVP (static arrows only). -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: canvas edge drawing and cycle-block toast pending Frontend; condition step label enforcement on connection pending Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** canvas edge drawing and cycle-block toast pending Frontend; condition step label enforcement on connection pending Frontend. --- @@ -93,8 +116,17 @@ A node-based drag-and-drop canvas (powered by React Flow) where users design the *Out of scope* - Full-screen step config modal — the panel-based UI is the only config surface in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: slide-over panel, inline error indicators, and auto-save pending Frontend; step config stored as JSONB dict in `steps` column. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** slide-over panel, inline error indicators, and auto-save pending Frontend; step config stored as JSONB dict in `steps` column. --- @@ -120,7 +152,13 @@ A node-based drag-and-drop canvas (powered by React Flow) where users design the *Out of scope* - Touch/gesture controls for tablet use — not in MVP. -> **Implementation status** — N/A (frontend-only) | API: N/A | Frontend: ⏳ +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | API | N/A | +> | Frontend | ⏳ | +> > No backend domain or infrastructure work; canvas navigation is a pure client-side concern. --- @@ -148,5 +186,11 @@ A node-based drag-and-drop canvas (powered by React Flow) where users design the *Out of scope* - Server-side version history with named snapshots — not in MVP. -> **Implementation status** — N/A (frontend-only) | API: N/A | Frontend: ⏳ +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | API | N/A | +> | Frontend | ⏳ | +> > Undo/redo is in-memory client state only; no server round-trip required. diff --git a/docs/epics/E04-workflow-builder/features/F03-step-types.md b/docs/epics/E04-workflow-builder/features/F03-step-types.md index df92035a..b92e7994 100644 --- a/docs/epics/E04-workflow-builder/features/F03-step-types.md +++ b/docs/epics/E04-workflow-builder/features/F03-step-types.md @@ -1,6 +1,11 @@ # F03 — Step Type Configuration -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflow-editor.excalidraw](../wireframes/workflow-editor.excalidraw) · [preview](../wireframes/workflow-editor.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflow-editor | [source](../wireframes/workflow-editor.excalidraw) | [preview](../wireframes/workflow-editor.svg) | + [← Back to E04](../README.md) @@ -37,9 +42,18 @@ Each step has a type that determines what it does when executed. Users configure *Out of scope* - Multiple assignees on a single Form step (assign to all and wait for the first response) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: form picker UI, assignee expression evaluation, and timeout enforcement pending Frontend + E06. -> Decisions: step config (formId, assignee, timeout) stored as JSONB dict in `steps` column. `StepType` enum includes `Start` and `End` values. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** form picker UI, assignee expression evaluation, and timeout enforcement pending Frontend + E06. +> **Decisions:** step config (formId, assignee, timeout) stored as JSONB dict in `steps` column. `StepType` enum includes `Start` and `End` values. --- @@ -69,8 +83,17 @@ Each step has a type that determines what it does when executed. Users configure - GraphQL or gRPC step types — not in MVP. - Response streaming — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: HTTP execution and Test request button pending E06 + Frontend; credential storage redaction is enforced at export (keys matching token/api_key/secret/password/authorization/etc. replaced with `[REDACTED]` in `ExportWorkflowHandler`). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** HTTP execution and Test request button pending E06 + Frontend; credential storage redaction is enforced at export (keys matching token/api_key/secret/password/authorization/etc. replaced with `[REDACTED]` in `ExportWorkflowHandler`). --- @@ -98,8 +121,17 @@ Each step has a type that determines what it does when executed. Users configure *Out of scope* - Raw expression editing (writing code directly) — the visual builder is the only interface in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: expression builder UI and branch evaluation pending Frontend + E06; condition branches stored in step config JSONB. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** expression builder UI and branch evaluation pending Frontend + E06; condition branches stored in step config JSONB. --- @@ -129,8 +161,17 @@ Each step has a type that determines what it does when executed. Users configure - Importing external npm packages — not in MVP. - Python or other language scripts — JavaScript only in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: JS sandbox execution, timeout enforcement, and "Run test" button pending E06 + Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** JS sandbox execution, timeout enforcement, and "Run test" button pending E06 + Frontend. --- @@ -160,5 +201,14 @@ Each step has a type that determines what it does when executed. Users configure - SMS, Slack, or Teams notification channels — not in MVP. - Notification templates shared across workflows — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: email/webhook dispatch pending E06; configurable fail-on-error toggle not yet implemented in API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⚠️ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** email/webhook dispatch pending E06; configurable fail-on-error toggle not yet implemented in API layer. diff --git a/docs/epics/E04-workflow-builder/features/F04-triggers.md b/docs/epics/E04-workflow-builder/features/F04-triggers.md index 856fd973..5f9a9baf 100644 --- a/docs/epics/E04-workflow-builder/features/F04-triggers.md +++ b/docs/epics/E04-workflow-builder/features/F04-triggers.md @@ -1,6 +1,11 @@ # F04 — Trigger Configuration -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflow-editor.excalidraw](../wireframes/workflow-editor.excalidraw) · [preview](../wireframes/workflow-editor.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflow-editor | [source](../wireframes/workflow-editor.excalidraw) | [preview](../wireframes/workflow-editor.svg) | + [← Back to E04](../README.md) @@ -35,9 +40,18 @@ A workflow must have at least one trigger before it can be published. Triggers d *Out of scope* - Triggering with pre-filled input from a page button (Page Builder) — not in MVP for this epic; covered in E07. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: input variable prompt dialog and `POST /workflows/{id}/executions` endpoint pending API + E06. -> Decisions: trigger config (input variable definitions) stored as JSONB in `triggers` column; domain guards against duplicate trigger type per workflow (AddTrigger returns Conflict on second call for same type). `TriggerConfig` is a value object (no `id`, owned by `WorkflowDefinition`). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** input variable prompt dialog and `POST /workflows/{id}/executions` endpoint pending API + E06. +> **Decisions:** trigger config (input variable definitions) stored as JSONB in `triggers` column; domain guards against duplicate trigger type per workflow (AddTrigger returns Conflict on second call for same type). `TriggerConfig` is a value object (no `id`, owned by `WorkflowDefinition`). --- @@ -66,8 +80,17 @@ A workflow must have at least one trigger before it can be published. Triggers d *Out of scope* - Date-specific one-time scheduling (e.g., "run once on 2026-12-25") — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: Wolverine cron job registration on publish and deregistration on archive pending E06; cron expression validation (min 5-min interval, IANA timezone) pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Wolverine cron job registration on publish and deregistration on archive pending E06; cron expression validation (min 5-min interval, IANA timezone) backend polish — see gaps below. --- @@ -96,8 +119,17 @@ A workflow must have at least one trigger before it can be published. Triggers d - GET webhook triggers — POST only in MVP. - Event-type filtering on a single webhook URL (multiple workflows sharing one URL) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: unique webhook URL generation, HMAC verification, and payload mapping pending E06 + API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** unique webhook URL generation, HMAC verification, and payload mapping pending E06 + API layer. --- @@ -127,5 +159,14 @@ A workflow must have at least one trigger before it can be published. Triggers d - Custom platform events defined by users — not in MVP. - Listening to events from external systems (without going through a Webhook trigger) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: Wolverine event subscription wiring and filter expression evaluation pending E06; event type registry and model-picker UI pending API + Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Wolverine event subscription wiring and filter expression evaluation pending E06; event type registry and model-picker UI pending API + Frontend. diff --git a/docs/epics/E04-workflow-builder/features/F05-branching.md b/docs/epics/E04-workflow-builder/features/F05-branching.md index 7c82fdb1..6b61d4d5 100644 --- a/docs/epics/E04-workflow-builder/features/F05-branching.md +++ b/docs/epics/E04-workflow-builder/features/F05-branching.md @@ -1,6 +1,11 @@ # F05 — Branching & Conditional Logic -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflow-editor.excalidraw](../wireframes/workflow-editor.excalidraw) · [preview](../wireframes/workflow-editor.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflow-editor | [source](../wireframes/workflow-editor.excalidraw) | [preview](../wireframes/workflow-editor.svg) | + [← Back to E04](../README.md) @@ -34,9 +39,18 @@ Workflows can take different execution paths based on data values using Conditio *Out of scope* - Loop-back branching (sending execution back to an earlier step) — cycles are blocked in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: canvas branch label rendering pending Frontend; branch evaluation at execution time pending E06. -> Decisions: cycle detection implemented in domain (DFS reachability check in AddTransition). `Transition` is a value object (only `fromStepId`, `toStepId`, `condition` — no identity or ordering fields). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** canvas branch label rendering pending Frontend; branch evaluation at execution time pending E06. +> **Decisions:** cycle detection implemented in domain (DFS reachability check in AddTransition). `Transition` is a value object (only `fromStepId`, `toStepId`, `condition` — no identity or ordering fields). --- @@ -64,8 +78,17 @@ Workflows can take different execution paths based on data values using Conditio *Out of scope* - Regex-based branch matching — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: branch drag-to-reorder UI and default-branch validation at publish pending Frontend + API. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** branch drag-to-reorder UI and default-branch validation at publish pending Frontend + API. --- @@ -90,5 +113,14 @@ Workflows can take different execution paths based on data values using Conditio *Out of scope* - Explicit merge/join nodes on the canvas — merging is implicit (any step with multiple incoming edges acts as a merge point). An explicit Join node is used only in Parallel Groups (F06). -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: OR-merge deduplication (execute-once on first arrival) is an execution engine concern — pending E06. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** OR-merge deduplication (execute-once on first arrival) is an execution engine concern — pending E06. diff --git a/docs/epics/E04-workflow-builder/features/F06-parallel-execution.md b/docs/epics/E04-workflow-builder/features/F06-parallel-execution.md index c40d65eb..e3319280 100644 --- a/docs/epics/E04-workflow-builder/features/F06-parallel-execution.md +++ b/docs/epics/E04-workflow-builder/features/F06-parallel-execution.md @@ -1,6 +1,11 @@ # F06 — Parallel Step Execution -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflow-editor.excalidraw](../wireframes/workflow-editor.excalidraw) · [preview](../wireframes/workflow-editor.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflow-editor | [source](../wireframes/workflow-editor.excalidraw) | [preview](../wireframes/workflow-editor.svg) | + [← Back to E04](../README.md) @@ -36,8 +41,17 @@ Multiple steps can run concurrently inside a Parallel Group. The workflow fans o *Out of scope* - Dynamic parallelism (creating N parallel branches based on a list of records at runtime) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: canvas container node rendering and step nesting UI pending Frontend; parallel group represented via step config in existing JSONB storage. `ParallelGroup` and `JoinType` are Phase 2 — shown as planned (dashed) in diagram. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** canvas container node rendering and step nesting UI pending Frontend; parallel group represented via step config in existing JSONB storage. `ParallelGroup` and `JoinType` are Phase 2 — shown as planned (dashed) in diagram. --- @@ -64,8 +78,17 @@ Multiple steps can run concurrently inside a Parallel Group. The workflow fans o *Out of scope* - "Wait for N of M" join type (e.g., wait for 2 out of 3 branches) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: AND/OR join execution, branch cancellation, and grace period pending E06. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** AND/OR join execution, branch cancellation, and grace period pending E06. --- @@ -89,5 +112,14 @@ Multiple steps can run concurrently inside a Parallel Group. The workflow fans o *Out of scope* - Merging/reducing outputs from parallel branches with built-in aggregation functions — not in MVP; use a Script step after the group for custom aggregation. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: context namespacing by step ID and sibling-output blocking pending E06; design-time duplicate output warning pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** context namespacing by step ID and sibling-output blocking pending E06; design-time duplicate output warning backend polish — see gaps below. diff --git a/docs/epics/E04-workflow-builder/features/F07-import-export.md b/docs/epics/E04-workflow-builder/features/F07-import-export.md index 0715ee5f..e6fcf2c9 100644 --- a/docs/epics/E04-workflow-builder/features/F07-import-export.md +++ b/docs/epics/E04-workflow-builder/features/F07-import-export.md @@ -1,6 +1,11 @@ # F07 — Workflow Import / Export -> **Wireframe**: [docs/epics/E04-workflow-builder/wireframes/workflows.excalidraw](../wireframes/workflows.excalidraw) · [preview](../wireframes/workflows.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| workflows | [source](../wireframes/workflows.excalidraw) | [preview](../wireframes/workflows.svg) | + [← Back to E04](../README.md) @@ -34,9 +39,18 @@ Workflow definitions can be exported as portable JSON files and imported into an - Exporting execution history — definitions only. - Exporting to formats other than JSON (YAML, BPMN) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: broken-reference `"broken": true` flag pending E03/E05 integration; export notice and broken-reference warning UI pending Frontend. -> Decisions: credential scrubbing in `ExportWorkflowHandler` — keys matching token/api_key/apikey/secret/password/authorization/auth_token/hmac_secret/client_secret/private_key/bearer/access_token/refresh_token replaced with `[REDACTED]` (OrdinalIgnoreCase). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** broken-reference `"broken": true` flag pending E03/E05 integration; export notice and broken-reference warning UI pending Frontend. +> **Decisions:** credential scrubbing in `ExportWorkflowHandler` — keys matching token/api_key/apikey/secret/password/authorization/auth_token/hmac_secret/client_secret/private_key/bearer/access_token/refresh_token replaced with `[REDACTED]` (OrdinalIgnoreCase). --- @@ -66,8 +80,17 @@ Workflow definitions can be exported as portable JSON files and imported into an *Out of scope* - Automatic periodic export/backup — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: import preview dialog, form/model resolution (auto-create or prompt), and file-picker UI pending Frontend; handler skips invalid transitions/triggers rather than stopping — full transactional rollback not yet implemented in the API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⚠️ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** import preview dialog, form/model resolution (auto-create or prompt), and file-picker UI pending Frontend; handler skips invalid transitions/triggers rather than stopping — full transactional rollback not yet implemented in the API layer. --- @@ -91,5 +114,14 @@ Workflow definitions can be exported as portable JSON files and imported into an *Out of scope* - Scheduled automatic backups — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: async notification for large exports (> 20 workflows) and org-slug prefix in ZIP filename pending API; empty-org README.txt and file-picker UI pending Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⚠️ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** async notification for large exports (> 20 workflows) and org-slug prefix in ZIP filename pending API; empty-org README.txt and file-picker UI pending Frontend. diff --git a/docs/epics/E05-form-builder/features/F01-form-definition.md b/docs/epics/E05-form-builder/features/F01-form-definition.md index a757080c..b8e0efa5 100644 --- a/docs/epics/E05-form-builder/features/F01-form-definition.md +++ b/docs/epics/E05-form-builder/features/F01-form-definition.md @@ -1,6 +1,11 @@ # F01 — Form Definition Management -> **Wireframe**: [docs/epics/E05-form-builder/wireframes/forms.excalidraw](../wireframes/forms.excalidraw) · [preview](../wireframes/forms.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| forms | [source](../wireframes/forms.excalidraw) | [preview](../wireframes/forms.svg) | + [← Back to E05](../README.md) @@ -34,9 +39,18 @@ Users can create, edit, and delete form definitions. A form is a reusable collec *Out of scope* - Form templates / starter library — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: live preview panel and form editor pending Frontend. -> Decisions: all form fields stored as JSONB via custom FormFieldConverter using FormFieldType as polymorphic discriminator. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** live preview panel and form editor pending Frontend. +> **Decisions:** all form fields stored as JSONB via custom FormFieldConverter using FormFieldType as polymorphic discriminator. --- @@ -61,9 +75,18 @@ Users can create, edit, and delete form definitions. A form is a reusable collec *Out of scope* - Folders/categories for organizing forms — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: "Used in N workflow(s)" count pending cross-module query — not supported at Application layer without inter-module dependency; deferred to API/Frontend aggregation. -> Decisions: `GetFormsHandler` paginates in-memory (GetAllAsync + LINQ Skip/Take). This is an accepted trade-off for MVP: adding a `GetPagedAsync` repository method would push sorting/paging logic into Infrastructure without additional correctness benefit at this scale. `Page` and `PageSize` are clamped to ≥ 1 and ≤ 100 in the handler. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** "Used in N workflow(s)" count pending cross-module query — not supported at Application layer without inter-module dependency; deferred to API/Frontend aggregation. +> **Decisions:** `GetFormsHandler` paginates in-memory (GetAllAsync + LINQ Skip/Take). This is an accepted trade-off for MVP: adding a `GetPagedAsync` repository method would push sorting/paging logic into Infrastructure without additional correctness benefit at this scale. `Page` and `PageSize` are clamped to ≥ 1 and ≤ 100 in the handler. --- @@ -88,9 +111,18 @@ Users can create, edit, and delete form definitions. A form is a reusable collec *Out of scope* - Form versioning (publishing a new "version" of a form) — not in MVP; edits are live immediately. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: live-workflow warning banner (informational) and definition snapshot for in-progress tasks pending API + E06. -> Decisions: `UpdateFormHandler` checks name uniqueness via `NameExistsAsync(name, orgId, excludeId)` before calling `form.Update()`; TOCTOU race requires a unique DB index on `(name, org_id)` at Infrastructure layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** live-workflow warning banner (informational) and definition snapshot for in-progress tasks pending API + E06. +> **Decisions:** `UpdateFormHandler` checks name uniqueness via `NameExistsAsync(name, orgId, excludeId)` before calling `form.Update()`; TOCTOU race requires a unique DB index on `(name, org_id)` at Infrastructure layer. --- @@ -115,6 +147,15 @@ Users can create, edit, and delete form definitions. A form is a reusable collec *Out of scope* - Recovering a soft-deleted form — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: HTTP 409 on delete-while-referenced enforced via `IsReferencedByWorkflowAsync` JSONB query across `workflow_definitions.steps`; archived-workflow exception pending API layer. -> Decisions: `IsReferencedByWorkflowAsync` uses raw SQL `workflow_definitions.steps @> [{...}]::jsonb` — cross-module table query within the same tenant schema. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** HTTP 409 on delete-while-referenced enforced via `IsReferencedByWorkflowAsync` JSONB query across `workflow_definitions.steps`; archived-workflow exception backend polish — see gaps below. +> **Decisions:** `IsReferencedByWorkflowAsync` uses raw SQL `workflow_definitions.steps @> [{...}]::jsonb` — cross-module table query within the same tenant schema. diff --git a/docs/epics/E05-form-builder/features/F02-form-fields.md b/docs/epics/E05-form-builder/features/F02-form-fields.md index bd3f6182..972568c9 100644 --- a/docs/epics/E05-form-builder/features/F02-form-fields.md +++ b/docs/epics/E05-form-builder/features/F02-form-fields.md @@ -1,6 +1,11 @@ # F02 — Form Field Configuration & Validation -> **Wireframe**: [docs/epics/E05-form-builder/wireframes/form-editor.excalidraw](../wireframes/form-editor.excalidraw) · [preview](../wireframes/form-editor.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| form-editor | [source](../wireframes/form-editor.excalidraw) | [preview](../wireframes/form-editor.svg) | + [← Back to E05](../README.md) @@ -39,9 +44,18 @@ Form fields define what data the form collects. Each field has a type, label, he *Out of scope* - Conditional field visibility (show field only if another field has a certain value) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: type picker UI, live preview update, and extension validation for File Upload pending Frontend + API layers. -> Decisions: `AddFieldToFormHandler` catches both `ArgumentException` (invalid key format) and `InvalidOperationException` (duplicate key) from the domain and returns `ErrorCodes.BusinessRule`. Field config polymorphism handled by FormFieldConverter using FormFieldType enum as discriminator. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** type picker UI, live preview update, and extension validation for File Upload pending Frontend + API layers. +> **Decisions:** `AddFieldToFormHandler` catches both `ArgumentException` (invalid key format) and `InvalidOperationException` (duplicate key) from the domain and returns `ErrorCodes.BusinessRule`. Field config polymorphism handled by FormFieldConverter using FormFieldType enum as discriminator. --- @@ -69,8 +83,17 @@ Form fields define what data the form collects. Each field has a type, label, he *Out of scope* - Cross-field validation (e.g., "end date must be after start date") — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: no `UpdateFieldValidationCommand` handler — field validation config is part of `AddFieldToForm` (set on creation only); client-side validation (React Hook Form + Zod) pending Frontend; HTTP 422 structured errors pending API layer. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** no `UpdateFieldValidationCommand` handler — field validation config is part of `AddFieldToForm` (set on creation only); client-side validation (React Hook Form + Zod) pending Frontend; HTTP 422 structured errors backend polish — see gaps below. --- @@ -95,9 +118,18 @@ Form fields define what data the form collects. Each field has a type, label, he *Out of scope* - Multi-column form layouts — not in MVP (single-column only). -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: drag-handle UI and real-time preview reorder pending Frontend. -> Decisions: `ReorderFormFieldsHandler` catches `ArgumentException` from domain (IDs don't match all fields) and returns `ErrorCodes.BusinessRule`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** drag-handle UI and real-time preview reorder pending Frontend. +> **Decisions:** `ReorderFormFieldsHandler` catches `ArgumentException` from domain (IDs don't match all fields) and returns `ErrorCodes.BusinessRule`. --- @@ -122,6 +154,15 @@ Form fields define what data the form collects. Each field has a type, label, he *Out of scope* - Collapsible sections — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: section grouping visual rendering pending Frontend. -> Decisions: sections use `FormFieldType.Section` + `SectionFieldConfig` and flow through the same `AddFieldToFormCommand` as regular fields. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** section grouping visual rendering pending Frontend. +> **Decisions:** sections use `FormFieldType.Section` + `SectionFieldConfig` and flow through the same `AddFieldToFormCommand` as regular fields. diff --git a/docs/epics/E05-form-builder/features/F03-workflow-integration.md b/docs/epics/E05-form-builder/features/F03-workflow-integration.md index cfeb0c47..68711657 100644 --- a/docs/epics/E05-form-builder/features/F03-workflow-integration.md +++ b/docs/epics/E05-form-builder/features/F03-workflow-integration.md @@ -1,6 +1,11 @@ # F03 — Workflow Step Integration -> **Wireframe**: [docs/epics/E05-form-builder/wireframes/forms.excalidraw](../wireframes/forms.excalidraw) · [preview](../wireframes/forms.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| forms | [source](../wireframes/forms.excalidraw) | [preview](../wireframes/forms.svg) | + [← Back to E05](../README.md) @@ -36,9 +41,18 @@ Forms are attached to Form steps in a workflow. The engine creates a Form Task a *Out of scope* - Creating a new form from within the workflow canvas (must go to Form Builder) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: broken-step indicator pending Frontend + API. -> Decisions: `GetFormPickerQuery` returns all forms for the org as a flat list (Id, Name, FieldCount) ordered by name — used by the API form-step picker dropdown. `IsReferencedByWorkflowAsync` query supports the reference check. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** broken-step indicator pending Frontend + API. +> **Decisions:** `GetFormPickerQuery` returns all forms for the org as a flat list (Id, Name, FieldCount) ordered by name — used by the API form-step picker dropdown. `IsReferencedByWorkflowAsync` query supports the reference check. --- @@ -64,8 +78,17 @@ Forms are attached to Form steps in a workflow. The engine creates a Form Task a *Out of scope* - Hiding fields from the assignee while keeping them pre-populated (hidden fields) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: context expression input UI and expression evaluation at execution time pending Frontend + E06. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** context expression input UI and expression evaluation at execution time pending Frontend + E06. --- @@ -91,5 +114,14 @@ Forms are attached to Form steps in a workflow. The engine creates a Form Task a *Out of scope* - Saving form submission data directly to a Data Model record automatically — not in MVP (a subsequent Script or HTTP step can do this). -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: context variable population after submission and context variable picker pending E06. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** context variable population after submission and context variable picker pending E06. diff --git a/docs/epics/E05-form-builder/features/F04-form-submission.md b/docs/epics/E05-form-builder/features/F04-form-submission.md index a9b5246e..6f9454bf 100644 --- a/docs/epics/E05-form-builder/features/F04-form-submission.md +++ b/docs/epics/E05-form-builder/features/F04-form-submission.md @@ -1,6 +1,11 @@ # F04 — Form Submission Handling -> **Wireframe**: [docs/epics/E05-form-builder/wireframes/form-submission.excalidraw](../wireframes/form-submission.excalidraw) · [preview](../wireframes/form-submission.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| form-submission | [source](../wireframes/form-submission.excalidraw) | [preview](../wireframes/form-submission.svg) | + [← Back to E05](../README.md) @@ -37,9 +42,18 @@ When a workflow reaches a Form step, the engine creates a Form Task and notifies - Push notifications (mobile) — not in MVP. - Escalation notifications if the form is not submitted after X hours — not in MVP (timeout causes failure, not escalation). -> **Implementation status** — Domain: ✅ | Application: ⚠️ | Infrastructure: ⏳ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: No Application handlers for form submission management; email notification dispatch and in-app notification pending E06 + notification infrastructure; role member resolution pending Identity integration. -> Decisions: `FormSubmission` is a single aggregate combining task assignment (executionId, assigneeUserId, accessToken, expiresAt) and response data (submittedData, submittedAt). A separate FormTask entity would add no domain logic — the relationship is always 1:1 and both live within the same lifecycle (Pending → Submitted/Expired/Cancelled). Status enum is `FormSubmissionStatus`; `Submitted` used instead of `Completed` to name the action clearly. `AccessToken` is a `Guid` (unique URL key, not JWT); expiry enforced via `ExpiresAt` + `Expire()` domain method; `Expire()` is non-idempotent by design — idempotency handled at the caller level. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ⚠️ | +> | Infrastructure | ✅ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** No notification dispatch on assign; email/in-app notification pending E06 + notification infrastructure; role member resolution pending Identity integration. **Done:** `FormSubmission` aggregate + `FormStepReachedHandler` creates pending submission with access token. +> **Decisions:** `FormSubmission` is a single aggregate combining task assignment (executionId, assigneeUserId, accessToken, expiresAt) and response data (submittedData, submittedAt). A separate FormTask entity would add no domain logic — the relationship is always 1:1 and both live within the same lifecycle (Pending → Submitted/Expired/Cancelled). Status enum is `FormSubmissionStatus`; `Submitted` used instead of `Completed` to name the action clearly. `AccessToken` is a `Guid` (unique URL key, not JWT); expiry enforced via `ExpiresAt` + `Expire()` domain method; `Expire()` is non-idempotent by design — idempotency handled at the caller level. --- @@ -71,8 +85,17 @@ When a workflow reaches a Form step, the engine creates a Form Task and notifies - Saving a draft of the form and resuming later — not in MVP. - The assignee being able to add comments or annotations to the form submission — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: `SubmitFormByTokenCommand` + anonymous `POST /api/form-tasks/{token}/submit` ✅. Standalone form page, field validation UX, pre-signed file upload, and multi-tab deduplication pending Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `SubmitFormByTokenCommand` + anonymous `POST /api/form-tasks/{token}/submit` ✅. Standalone form page, field validation UX, pre-signed file upload, and multi-tab deduplication pending Frontend. --- @@ -100,8 +123,17 @@ When a workflow reaches a Form step, the engine creates a Form Task and notifies - Delegating a task to another user — not in MVP. - Bulk task completion — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ✅ | Frontend: ⏳ -> Gaps vs spec: **Done:** `GetMyFormTasksQuery` + authenticated list endpoints. Role-assigned task aggregation (not only direct assignee), SignalR push, and "My Tasks" UI pending Frontend + E06. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** **Done:** `GetMyFormTasksQuery` + authenticated list endpoints. Role-assigned task aggregation (not only direct assignee), SignalR push, and "My Tasks" UI pending Frontend + E06. --- @@ -128,5 +160,14 @@ When a workflow reaches a Form step, the engine creates a Form Task and notifies *Out of scope* - Sending a reminder notification before timeout expires — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ⚠️ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: **Done:** `ExpireFormSubmissionMessage` scheduled from `FormStepReachedHandler`; `ExpireFormSubmissionHandler` marks submission expired. Workflow execution → `Failed` + error notification on expiry pending E06 coordination. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ⚠️ | +> | Infrastructure | ✅ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** **Done:** `ExpireFormSubmissionMessage` scheduled from `FormStepReachedHandler`; `ExpireFormSubmissionHandler` marks submission expired. Workflow execution → `Failed` + error notification on expiry pending E06 coordination. diff --git a/docs/epics/E06-workflow-engine/features/F01-execution-management.md b/docs/epics/E06-workflow-engine/features/F01-execution-management.md index 660b057a..8c989383 100644 --- a/docs/epics/E06-workflow-engine/features/F01-execution-management.md +++ b/docs/epics/E06-workflow-engine/features/F01-execution-management.md @@ -1,6 +1,11 @@ # F01 — Execution Management -> **Wireframe**: [docs/epics/E06-workflow-engine/wireframes/executions.excalidraw](../wireframes/executions.excalidraw) · [preview](../wireframes/executions.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| executions | [source](../wireframes/executions.excalidraw) | [preview](../wireframes/executions.svg) | + [← Back to E06](../README.md) @@ -38,9 +43,18 @@ The engine manages the full lifecycle of a workflow execution — from creation *Out of scope* - Triggering a specific version of a workflow (other than the current active version) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: trigger HTTP endpoint, schedule/webhook/event trigger handlers, stale-PENDING recovery job pending API + E06 engine. -> Decisions: `WorkflowExecution.Create` sets status `Pending`; `Start()` transitions to `Running` — engine calls both in sequence. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** trigger HTTP endpoint, schedule/webhook/event trigger handlers, stale-PENDING recovery job pending API + E06 engine. +> **Decisions:** `WorkflowExecution.Create` sets status `Pending`; `Start()` transitions to `Running` — engine calls both in sequence. --- @@ -68,9 +82,18 @@ The engine manages the full lifecycle of a workflow execution — from creation *Out of scope* - Real-time execution graph overlay on the workflow canvas — not in MVP (timeline list only). -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: SignalR push updates and execution detail page pending Frontend + API. -> Decisions: `GetExecutionHandler` delegates to `IExecutionRepository.GetWithStepsAsync`, which loads execution + steps in two queries (no EF navigation property — `ExecutionStep` is a separate aggregate). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** SignalR push updates and execution detail page pending Frontend. **Done:** `GET /api/executions/{id}` returns step timeline via `GetExecutionHandler`. +> **Decisions:** `GetExecutionHandler` delegates to `IExecutionRepository.GetWithStepsAsync`, which loads execution + steps in two queries (no EF navigation property — `ExecutionStep` is a separate aggregate). --- @@ -99,6 +122,15 @@ The engine manages the full lifecycle of a workflow execution — from creation *Out of scope* - Pausing an execution and resuming it — not in MVP (cancel only). -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: `POST /api/executions/{id}/cancel` ✅. Cancel button UI, Wolverine job abandonment, and Form Task cancellation pending engine. -> Decisions: `Cancel()` domain guard rejects terminal statuses (`Completed`, `Failed`, `Cancelled`) with `InvalidOperationException`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `POST /api/executions/{id}/cancel` ✅. Cancel button UI, Wolverine job abandonment, and Form Task cancellation pending engine. +> **Decisions:** `Cancel()` domain guard rejects terminal statuses (`Completed`, `Failed`, `Cancelled`) with `InvalidOperationException`. diff --git a/docs/epics/E06-workflow-engine/features/F02-step-handlers.md b/docs/epics/E06-workflow-engine/features/F02-step-handlers.md index 93d157f1..57d42e44 100644 --- a/docs/epics/E06-workflow-engine/features/F02-step-handlers.md +++ b/docs/epics/E06-workflow-engine/features/F02-step-handlers.md @@ -1,6 +1,11 @@ # F02 — Step Execution Handlers -> **Wireframe**: [docs/epics/E06-workflow-engine/wireframes/execution-detail.excalidraw](../wireframes/execution-detail.excalidraw) · [preview](../wireframes/execution-detail.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| execution-detail | [source](../wireframes/execution-detail.excalidraw) | [preview](../wireframes/execution-detail.svg) | + [← Back to E06](../README.md) @@ -92,9 +97,18 @@ Each step type has a dedicated handler that executes it in isolation. Handlers r *Out of scope* - Custom step types defined by users — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ⚠️ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ⚠️ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** > - AC "concurrent delivery: second detects Running and exits" — spec wording updated in decision below; concurrent-duplicate protection is implemented via `UseXminAsConcurrencyToken()` on `execution_steps` rows. The second concurrent writer receives a `DbUpdateConcurrencyException` (translated to `ConcurrencyException`), logs, and exits gracefully. The "Running guard" approach was deliberately rejected — see Decisions. > - Engine-level 5-minute step timeout not yet implemented (E06 F03 scope). > - `IScriptExecutor` and `INotificationSender` are stubs returning empty/success; real JS sandbox engine and email/webhook dispatch are deferred. -> Decisions: `ExecutionStep.IsTerminal` covers Completed/Failed/Cancelled. `Skipped` is a pre-start terminal state set by the Condition handler for rejected branches. `StepType` is an enum (Start, End, Form, HttpRequest, Condition, Script, Notification). `WorkflowSnapshot` is a local read model populated from `WorkflowPublished` domain events — WorkflowEngine never queries WorkflowBuilder's DB. Concurrent-duplicate protection uses PostgreSQL `xmin` system column (`UseXminAsConcurrencyToken()` on the owned `execution_steps` table) — no migration required. All step handlers and `StepCompletedHandler`/`StepFailedHandler` catch `ConcurrencyException` from `IUnitOfWork.SaveChangesAsync` and exit gracefully, letting the winning instance complete the step. The Running guard (`if step.Status == Running → skip`) was rejected because `ExecuteNextStepHandler` always starts a step (→ Running) before dispatching; a Running guard would block all normal first-delivery executions. +> **Decisions:** `ExecutionStep.IsTerminal` covers Completed/Failed/Cancelled. `Skipped` is a pre-start terminal state set by the Condition handler for rejected branches. `StepType` is an enum (Start, End, Form, HttpRequest, Condition, Script, Notification). `WorkflowSnapshot` is a local read model populated from `WorkflowPublished` domain events — WorkflowEngine never queries WorkflowBuilder's DB. Concurrent-duplicate protection uses PostgreSQL `xmin` system column (`UseXminAsConcurrencyToken()` on the owned `execution_steps` table) — no migration required. All step handlers and `StepCompletedHandler`/`StepFailedHandler` catch `ConcurrencyException` from `IUnitOfWork.SaveChangesAsync` and exit gracefully, letting the winning instance complete the step. The Running guard (`if step.Status == Running → skip`) was rejected because `ExecuteNextStepHandler` always starts a step (→ Running) before dispatching; a Running guard would block all normal first-delivery executions. diff --git a/docs/epics/E06-workflow-engine/features/F03-error-handling.md b/docs/epics/E06-workflow-engine/features/F03-error-handling.md index 8fce27c0..387362c2 100644 --- a/docs/epics/E06-workflow-engine/features/F03-error-handling.md +++ b/docs/epics/E06-workflow-engine/features/F03-error-handling.md @@ -1,6 +1,11 @@ # F03 — Error Handling & Notification -> **Wireframe**: [docs/epics/E06-workflow-engine/wireframes/execution-detail.excalidraw](../wireframes/execution-detail.excalidraw) · [preview](../wireframes/execution-detail.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| execution-detail | [source](../wireframes/execution-detail.excalidraw) | [preview](../wireframes/execution-detail.svg) | + [← Back to E06](../README.md) @@ -36,8 +41,17 @@ When a step fails, the engine marks the execution as `FAILED`, records full erro *Out of scope* - PagerDuty / OpsGenie / Slack integration for error notifications — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ⚠️ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: no Application-layer notification dispatch handler; `ExecutionFailed` domain event raised but notification channels not wired; email/in-app/webhook dispatch and rate-limiting pending Application layer + a future cross-cutting notification service (outside WorkflowEngine Infrastructure, which is complete). +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ⚠️ | +> | Infrastructure | ✅ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** no Application-layer notification dispatch handler; `ExecutionFailed` domain event raised but notification channels not wired; email/in-app/webhook dispatch and rate-limiting pending Application layer + a future cross-cutting notification service (outside WorkflowEngine Infrastructure, which is complete). --- @@ -64,8 +78,17 @@ When a step fails, the engine marks the execution as `FAILED`, records full erro *Out of scope* - Sharing a link to a specific error detail view with another user — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ⚠️ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: `GetExecutionQuery` (returning step-level error details) not yet implemented; error detail UI (stack trace, redacted fields) pending Frontend + API. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ⚠️ | +> | Infrastructure | ✅ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `GetExecutionQuery` (returning step-level error details) not yet implemented; error detail UI (stack trace, redacted fields) pending Frontend + API. --- @@ -92,5 +115,14 @@ When a step fails, the engine marks the execution as `FAILED`, records full erro *Out of scope* - Different notification channels for different failure scenarios (e.g., "only notify on HTTP step failures") — not in MVP; all failures use the same channels. -> **Implementation status** — Domain: ⚠️ | Application: ⚠️ | Infrastructure: ⏳ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: error notification channel config is not modeled in the domain (no channel list on `WorkflowExecution`); no `UpdateErrorNotificationChannelsCommand` handler; notification channel configuration UI and per-workflow channel storage pending API + Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ⚠️ | +> | Application | ⚠️ | +> | Infrastructure | ⏳ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** error notification channel config is not modeled in the domain (no channel list on `WorkflowExecution`); no `UpdateErrorNotificationChannelsCommand` handler; notification channel configuration UI and per-workflow channel storage pending API + Frontend. diff --git a/docs/epics/E06-workflow-engine/features/F04-execution-history.md b/docs/epics/E06-workflow-engine/features/F04-execution-history.md index acc04dba..c4186759 100644 --- a/docs/epics/E06-workflow-engine/features/F04-execution-history.md +++ b/docs/epics/E06-workflow-engine/features/F04-execution-history.md @@ -1,6 +1,11 @@ # F04 — Execution History & Audit Log -> **Wireframe**: [docs/epics/E06-workflow-engine/wireframes/executions.excalidraw](../wireframes/executions.excalidraw) · [preview](../wireframes/executions.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| executions | [source](../wireframes/executions.excalidraw) | [preview](../wireframes/executions.svg) | + [← Back to E06](../README.md) @@ -37,9 +42,18 @@ Every workflow execution and each of its steps is recorded in full detail. Users *Out of scope* - Execution analytics dashboard (charts, trends) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: `GET /api/executions` and per-workflow paged list ✅. Filter UI, date/trigger query params, and running-first sort pending API + Frontend. -> Decisions: `GetExecutionsByWorkflowHandler` and `GetAllExecutionsHandler` use `IExecutionRepository.GetPagedByWorkflowAsync`/`GetPagedAsync` — server-side pagination with `pageSize` clamped to 100. Status filter forwarded to repository. Date range filter and trigger type filter deferred to API layer query parameters. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `GET /api/executions` and per-workflow paged list ✅. Filter UI, date/trigger query params, and running-first sort pending API + Frontend. +> **Decisions:** `GetExecutionsByWorkflowHandler` and `GetAllExecutionsHandler` use `IExecutionRepository.GetPagedByWorkflowAsync`/`GetPagedAsync` — server-side pagination with `pageSize` clamped to 100. Status filter forwarded to repository. Date range filter and trigger type filter deferred to API layer query parameters. --- @@ -66,8 +80,17 @@ Every workflow execution and each of its steps is recorded in full detail. Users *Out of scope* - Replaying or simulating an execution from any point with a different context — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: Step timeline UI, context snapshot display, and parallel group rendering pending Frontend + API. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** Step timeline UI, context snapshot display, and parallel group rendering pending Frontend. **Done:** `GET /api/executions/{id}` returns execution + steps. --- @@ -92,8 +115,17 @@ Every workflow execution and each of its steps is recorded in full detail. Users *Out of scope* - Platform-wide execution monitoring across all tenants (Platform Admin view) — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⏳ | Frontend: ⏳ -> Gaps vs spec: CSV export, role-scoped visibility enforcement, and async export notification pending API + Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ⚠️ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** **Done:** `GET /api/executions` org-wide list (paginated). CSV export, role-scoped visibility enforcement, and async export notification pending API + Frontend. --- diff --git a/docs/epics/E06-workflow-engine/features/F05-manual-retry.md b/docs/epics/E06-workflow-engine/features/F05-manual-retry.md index 2ca8ebe6..b050f339 100644 --- a/docs/epics/E06-workflow-engine/features/F05-manual-retry.md +++ b/docs/epics/E06-workflow-engine/features/F05-manual-retry.md @@ -1,6 +1,11 @@ # F05 — Manual Retry -> **Wireframe**: [docs/epics/E06-workflow-engine/wireframes/execution-detail.excalidraw](../wireframes/execution-detail.excalidraw) · [preview](../wireframes/execution-detail.svg) +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| execution-detail | [source](../wireframes/execution-detail.excalidraw) | [preview](../wireframes/execution-detail.svg) | + [← Back to E06](../README.md) @@ -39,9 +44,18 @@ When a workflow execution fails at a step, users can manually retry from the fai *Out of scope* - Automatic retry (without user action) — not in MVP. -> **Implementation status** — Domain + Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: `POST /api/executions/{id}/retry` and retry-with-context ✅. Retry UI and archived-definition warning pending Frontend. -> Decisions: `CreateRetry()` produces a new `WorkflowExecution` with `RetryOfExecutionId` set; context is copied from original at time of retry. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `POST /api/executions/{id}/retry` and retry-with-context ✅. Retry UI and archived-definition warning pending Frontend. +> **Decisions:** `CreateRetry()` produces a new `WorkflowExecution` with `RetryOfExecutionId` set; context is copied from original at time of retry. --- @@ -67,8 +81,17 @@ When a workflow execution fails at a step, users can manually retry from the fai *Out of scope* - Comparing two retry attempts side-by-side — not in MVP. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: `GET /api/executions/{id}/retry-history` ✅. Retry history UI and interlinked execution chain navigation pending Frontend. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `GET /api/executions/{id}/retry-history` ✅. Retry history UI and interlinked execution chain navigation pending Frontend. --- @@ -95,6 +118,15 @@ When a workflow execution fails at a step, users can manually retry from the fai *Out of scope* - Structured field-by-field editing of context (showing fields by step/variable name) — not in MVP; raw JSON editor only. -> **Implementation status** — Domain: ✅ | Application: ✅ | Infrastructure: ✅ | API: ⚠️ | Frontend: ⏳ -> Gaps vs spec: `POST /api/executions/{id}/retry-with-context` ✅. JSON context editor UI and modified-context flag pending Frontend. -> Decisions: `CreateRetryWithModifiedContext` added to domain as private `CreateRetryCore` delegation — shares validation logic with `CreateRetry`. +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** `POST /api/executions/{id}/retry-with-context` ✅. JSON context editor UI and modified-context flag pending Frontend. +> **Decisions:** `CreateRetryWithModifiedContext` added to domain as private `CreateRetryCore` delegation — shares validation logic with `CreateRetry`. diff --git a/docs/epics/README.md b/docs/epics/README.md index b6576348..356a7cd7 100644 --- a/docs/epics/README.md +++ b/docs/epics/README.md @@ -13,11 +13,22 @@ | 1 | This page → **Open work** on the epic README | Prioritized gaps for that epic (backend vs frontend called out) | | 2 | `docs/epics/{epic}/features/F0N-*.md` | Per–user-story `> **Implementation status**` + `Gaps vs spec` + `**Done:**` / `**Deferred:**` | | 3 | `docs/PROGRESS.md` | Module layer summary (Domain → Frontend); cross-cutting foundation phases | -| 4 | `grep -r "Application: ⚠️\|Infrastructure: ⚠️" docs/epics/` | US rows still blocked before API work ([agent-checklist](../playbooks/agent-checklist.md)) | +| 4 | `grep -rE "\\| Application \\| ⚠️\\|\\| Infrastructure \\| ⚠️\\|\\| API \\| ⚠️" docs/epics/` | US rows with partial backend layers ([agent-checklist](../playbooks/agent-checklist.md)) | -**Symbols** (same as [agent-checklist § Layer status](../playbooks/agent-checklist.md)): ✅ done for that layer on this US · ⚠️ partial (read `Gaps vs spec`) · ⏳ not started. +**Symbols** (per layer **on this US**, not the whole module): -When you ship code, update **US callout → epic README table → epic Open work → PROGRESS** in the same PR. Never mark ✅ while `Gaps vs spec` still lists backend work for that layer. +| Symbol | Meaning | +|--------|---------| +| ✅ | All **in-scope backend AC** for this layer on this US are implemented (or Frontend-only AC when layer is Frontend). | +| ⚠️ | Layer partially shipped — read `**Gaps vs spec**` for what remains on this US. | +| ⏳ | Layer not started for this US. | +| N/A | Layer does not apply (e.g. Frontend-only US → Domain N/A). | + +**Epic README `API ✅ Done`** = core REST routes exist for the module. **US callout `API`** = AC-level truth for that story (filters, SignalR, CSV export, etc. may still be ⚠️/⏳). + +When you ship code, update **US callout → epic README table → epic Open work → PROGRESS** in the same PR. Never mark ✅ while `**Gaps vs spec**` still lists backend work for that layer. + +**Feature file layout:** wireframes as a `## Wireframes` table; implementation status as a blockquote + layer table — see [docs-style § Feature files](../playbooks/docs-style.md#feature-files--wireframes--implementation-status). After bulk edits, run `python3 scripts/normalize-feature-docs.py --check` (or omit `--check` to rewrite). **Full AC coverage (all cases, not happy path only):** [agent-checklist § AC coverage](../playbooks/agent-checklist.md#ac-coverage--avoid-happy-path-only) — Gate 0 AC map + TDD + `Gaps vs spec` on every PR. diff --git a/docs/epics/_template-feature-us.md b/docs/epics/_template-feature-us.md new file mode 100644 index 00000000..6ce7cf6a --- /dev/null +++ b/docs/epics/_template-feature-us.md @@ -0,0 +1,56 @@ +# F0N — Feature title + +[← Back to E0X](../README.md) + +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| login | [source](./E02-identity-access/wireframes/login.excalidraw) | [preview](./E02-identity-access/wireframes/login.svg) | + +--- + +## Description + +One paragraph describing the feature. + +--- + +## User Stories + +### US-000 — Story title + +**As a** role, **I want to** action **so that** outcome. + +**Acceptance Criteria:** + +*Happy path* +- [ ] … + +*Validation & errors* +- [ ] … + +*Edge cases* +- [ ] … + +*Out of scope* +- [ ] … + +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ⏳ | +> | Application | ⏳ | +> | Infrastructure | ⏳ | +> | API | ⏳ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** … +> **Done:** … +> **Deferred (PR #N follow-up):** … (omit line if none) +> **Decisions:** … + +--- + +**Copy this file** when adding a new feature US block. Layout rules: [docs-style § Feature files](../playbooks/docs-style.md#feature-files--wireframes--implementation-status). Validate with `python3 scripts/normalize-feature-docs.py --check`. diff --git a/docs/playbooks/agent-checklist.md b/docs/playbooks/agent-checklist.md index 4d3db454..17309700 100644 --- a/docs/playbooks/agent-checklist.md +++ b/docs/playbooks/agent-checklist.md @@ -105,7 +105,7 @@ Do **not** mark a layer ✅ or write `Gaps vs spec: none for backend` because th | `src/` or `tests/` | `dotnet format --verify-no-changes` | | `frontend/` | `npm run ci` then `npm run test` | | `src/Axis.Api/Endpoints/` or API contract | Update + run `tests/Api/Axis.Api.Tests/` | -| Any of the above + `docs/epics/` | `./scripts/check-doc-drift.sh` (bash) — also enforces no-new `TODO`/`FIXME`/`stub` and reviews new raw-SQL calls | +| Any of the above + `docs/epics/` | `./scripts/check-doc-drift.sh` (bash) — also runs `normalize-feature-docs.py --check`, enforces no-new `TODO`/`FIXME`/`stub`, reviews new raw-SQL calls | ```text Gate 1 self-check: @@ -128,7 +128,7 @@ Paste block format: header `Gate 2:` then one `-` line per row (Gate 3 uses the Gate 2: - Library → TECH_STACK.md / not triggered - New pattern → patterns.md / not triggered -- US layer callout → docs/epics/…/features/… / not triggered +- US layer callout → docs/epics/…/features/… (table layout per [docs-style § Feature files](./docs-style.md#feature-files--wireframes--implementation-status)) / not triggered - Epic README + PROGRESS → … / not triggered - Architecture rule → CLAUDE.md / not triggered - process.md workflow → … / not triggered diff --git a/docs/playbooks/docs-style.md b/docs/playbooks/docs-style.md index 6ad280d2..8db08371 100644 --- a/docs/playbooks/docs-style.md +++ b/docs/playbooks/docs-style.md @@ -40,6 +40,51 @@ When you find yourself editing the same fact in two files, the architecture is w --- +## Feature files — wireframes & implementation status + +Every `docs/epics/*/features/F0N-*.md` file uses these layouts so agents can scan status without parsing inline pipes. + +### Wireframes (top of file, after title / back-link) + +```markdown +## Wireframes + +| Screen | Excalidraw | Preview | +|--------|------------|---------| +| login | [source](../wireframes/login.excalidraw) | [preview](../wireframes/login.svg) | +``` + +One row per screen. **Do not** stack multiple `> **Wireframe**:` blockquote lines — they are hard to read and drift from the table format. + +### Implementation status (after each US AC block) + +```markdown +> **Implementation status** +> +> | Layer | Status | +> |-------|--------| +> | Domain | ✅ | +> | Application | ✅ | +> | Infrastructure | ✅ | +> | API | ✅ | +> | Frontend | ⏳ | +> +> **Gaps vs spec:** … +> **Done:** … +> **Deferred (PR #N follow-up):** … +> **Decisions:** … +``` + +Rules: + +- **One row per layer** — split `Domain + Application` into two rows. +- **`Gaps vs spec`** lists remaining AC bullets; never write `pending API layer` when endpoints already exist — say what is missing (`403 test`, `date filter query param`, etc.). +- **`API ✅`** on a US means in-scope REST/OpenAPI AC for that story are shipped; Frontend-only gaps do not downgrade API to ⚠️. + +**Bulk normalize:** `python3 scripts/normalize-feature-docs.py` (add `--check` for CI — also run via `check-doc-drift.sh`). + +--- + ## When you add a new `.md` file 1. Add the back-link header (per [`docs/README.md`](../README.md)): `> **Navigation**: [← parent.md](...)` so future readers can climb back up. diff --git a/scripts/check-doc-drift.sh b/scripts/check-doc-drift.sh index 02169b13..42277d6e 100755 --- a/scripts/check-doc-drift.sh +++ b/scripts/check-doc-drift.sh @@ -303,6 +303,24 @@ done < <( "${ROOT}/scripts/check-buf-modules.sh" || ERR=1 +# Feature file layout: table wireframes + table implementation status (docs-style.md). +python3 "${ROOT}/scripts/normalize-feature-docs.py" --check || ERR=1 + +if any_changed '^docs/epics/.*/features/.*\.md$'; then + while IFS= read -r match; do + [ -z "${match}" ] && continue + fail "Deprecated single-line Implementation status — use table layout; run: python3 scripts/normalize-feature-docs.py (${match})" + done < <( + grep -rnE '^\> \*\*Implementation status\*\* — ' docs/epics/ 2>/dev/null || true + ) + while IFS= read -r match; do + [ -z "${match}" ] && continue + fail "Deprecated inline Wireframe blockquote — use ## Wireframes table; run: python3 scripts/normalize-feature-docs.py (${match})" + done < <( + grep -rnE '^\> \*\*Wireframe\*\*:' docs/epics/ 2>/dev/null || true + ) +fi + if [ "${ERR}" -ne 0 ]; then echo "" >&2 echo "See docs/playbooks/agent-checklist.md" >&2 diff --git a/scripts/normalize-feature-docs.py b/scripts/normalize-feature-docs.py new file mode 100755 index 00000000..a60d08da --- /dev/null +++ b/scripts/normalize-feature-docs.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Normalize feature file wireframe lists and Implementation status callouts. + +Usage: + python3 scripts/normalize-feature-docs.py # rewrite files in place + python3 scripts/normalize-feature-docs.py --check # exit 1 if rewrite needed (CI) +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +FEATURE_GLOB = "docs/epics/**/features/*.md" + +LAYER_ORDER = [ + "Domain", + "Application", + "Infrastructure", + "Contracts", + "API", + "Frontend", +] + +WIREFRAME_LINE = re.compile( + r"^> \*\*Wireframe\*\*: \[(?:[^\]]+)\]\(([^)]+)\) · \[preview\]\(([^)]+)\)\s*$" +) + +STATUS_HEADER = re.compile( + r"^> \*\*Implementation status\*\* — (.+)\s*$" +) + +LAYER_PART = re.compile( + r"^(.+?): (.+)$" +) + +DEPRECATED_STATUS = re.compile(r"^\> \*\*Implementation status\*\* — ") +DEPRECATED_WIREFRAME = re.compile(r"^\> \*\*Wireframe\*\*:") + + +def slug_from_excalidraw(path: str) -> str: + name = Path(path).name + if name.endswith(".excalidraw"): + return name[: -len(".excalidraw")] + return name + + +def parse_layers(header_rest: str) -> list[tuple[str, str]]: + rows: list[tuple[str, str]] = [] + for part in header_rest.split(" | "): + part = part.strip() + match = LAYER_PART.match(part) + if not match: + continue + layer_label, status = match.group(1).strip(), match.group(2).strip() + if "+" in layer_label: + for layer in (item.strip() for item in layer_label.split("+")): + rows.append((layer, status)) + else: + rows.append((layer_label, status)) + return rows + + +def sort_layers(rows: list[tuple[str, str]]) -> list[tuple[str, str]]: + order_index = {name: index for index, name in enumerate(LAYER_ORDER)} + return sorted(rows, key=lambda item: order_index.get(item[0], len(LAYER_ORDER))) + + +def format_status_block(lines: list[str]) -> list[str]: + if not lines: + return lines + + header_match = STATUS_HEADER.match(lines[0]) + if not header_match: + return lines + + rows = sort_layers(parse_layers(header_match.group(1))) + body_lines = [line[2:].strip() for line in lines[1:] if line.startswith("> ")] + + out: list[str] = [ + "> **Implementation status**", + ">", + "> | Layer | Status |", + "> |-------|--------|", + ] + for layer, status in rows: + out.append(f"> | {layer} | {status} |") + out.append(">") + + for body in body_lines: + if body.startswith("Gaps vs spec:"): + out.append(f"> **Gaps vs spec:** {body[len('Gaps vs spec:'):].strip()}") + elif body.startswith("Decisions:"): + out.append(f"> **Decisions:** {body[len('Decisions:'):].strip()}") + elif body.startswith("**"): + out.append(f"> {body}") + else: + out.append(f"> {body}") + + return out + + +def format_wireframes(lines: list[str]) -> list[str] | None: + entries: list[tuple[str, str, str]] = [] + for line in lines: + match = WIREFRAME_LINE.match(line) + if not match: + return None + excalidraw, preview = match.group(1), match.group(2) + entries.append((slug_from_excalidraw(excalidraw), excalidraw, preview)) + + if not entries: + return None + + out = [ + "## Wireframes", + "", + "| Screen | Excalidraw | Preview |", + "|--------|------------|---------|", + ] + for slug, excalidraw, preview in entries: + out.append( + f"| {slug} | [source]({excalidraw}) | [preview]({preview}) |" + ) + out.append("") + return out + + +def normalize_content(text: str) -> str: + replacements = [ + ( + "pending API layer", + "backend polish — see gaps below", + ), + ( + "pending API query layer", + "query projection polish — see gaps below", + ), + ( + "pending auth infrastructure (OpenIddict + Redis)", + "auth infrastructure polish — see gaps below", + ), + ( + "pending API/Infrastructure concern", + "API/Infrastructure polish", + ), + ] + for old, new in replacements: + text = text.replace(old, new) + return text + + +def transform_text(original: str) -> tuple[str, bool]: + lines = original.splitlines() + changed = False + output: list[str] = [] + index = 0 + + while index < len(lines): + line = lines[index] + + if WIREFRAME_LINE.match(line): + wireframe_run = [line] + index += 1 + while index < len(lines) and WIREFRAME_LINE.match(lines[index]): + wireframe_run.append(lines[index]) + index += 1 + formatted = format_wireframes(wireframe_run) + if formatted is None: + output.extend(wireframe_run) + else: + output.extend(formatted) + changed = True + continue + + if STATUS_HEADER.match(line): + status_run = [line] + index += 1 + while index < len(lines) and lines[index].startswith("> ") and not STATUS_HEADER.match(lines[index]): + status_run.append(lines[index]) + index += 1 + formatted = format_status_block(status_run) + if formatted != status_run: + changed = True + output.extend(formatted) + continue + + output.append(line) + index += 1 + + new_text = normalize_content("\n".join(output) + ("\n" if original.endswith("\n") else "")) + if new_text != original: + changed = True + return new_text, changed + + +def check_deprecated_format(path: Path) -> list[str]: + issues: list[str] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if DEPRECATED_STATUS.match(line): + issues.append(f"{path}:{line_number}: deprecated single-line Implementation status") + if DEPRECATED_WIREFRAME.match(line): + issues.append(f"{path}:{line_number}: deprecated inline Wireframe blockquote") + return issues + + +def process_file(path: Path, check_only: bool) -> bool: + original = path.read_text(encoding="utf-8") + new_text, changed = transform_text(original) + if changed and not check_only: + path.write_text(new_text, encoding="utf-8") + return changed + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="Do not write files; exit 1 if any feature file needs normalization", + ) + args = parser.parse_args() + + changed_files: list[str] = [] + deprecated_issues: list[str] = [] + + for path in sorted(ROOT.glob(FEATURE_GLOB)): + if process_file(path, check_only=args.check): + changed_files.append(str(path.relative_to(ROOT))) + if args.check: + deprecated_issues.extend(check_deprecated_format(path)) + + exit_code = 0 + + if deprecated_issues: + exit_code = 1 + print("Deprecated feature file format detected:", file=sys.stderr) + for issue in deprecated_issues: + print(f" {issue}", file=sys.stderr) + print("Run: python3 scripts/normalize-feature-docs.py", file=sys.stderr) + + if changed_files: + exit_code = 1 + mode = "needs normalization" if args.check else "normalized" + print(f"Feature files {mode} ({len(changed_files)}):", file=sys.stderr) + for file_path in changed_files: + print(f" - {file_path}", file=sys.stderr) + if args.check: + print("Run: python3 scripts/normalize-feature-docs.py", file=sys.stderr) + else: + print(f"Normalized {len(changed_files)} feature file(s):") + for file_path in changed_files: + print(f" - {file_path}") + + if exit_code == 0: + print("normalize-feature-docs: OK") + return exit_code + + +if __name__ == "__main__": + sys.exit(main())