diff --git a/docs/STATUS.md b/docs/STATUS.md index c4e2e5b2..bd1de31d 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -30,10 +30,10 @@ These two commits collectively add: ### Recently landed (since the original architectural commits) - **AI-assisted coding workflow tooling** (PR #15, merged 2026-05-23) — `.claude/` and `.github/` gain a deliberate, demonstrable workflow layer. Three hooks (PreToolUse async-blocker, SessionStart STATUS.md inject, PostToolUse cross-reference checker). Three project slash commands (`/new-feature-slice`, `/sync-status`, `/check-rules`). One project agent (`architecture-reviewer`). Eight curated community skills, all audited via `skill-security-auditor` before install (skill-security-auditor itself, verification-before-completion, systematic-debugging, variant-analysis, test-driven-development, using-git-worktrees, writing-plans + executing-plans). Architecture map at [.claude/architecture-map.md](../.claude/architecture-map.md) — code-graph for both AI and human orientation. New GitHub layer: `PULL_REQUEST_TEMPLATE.md` (honest "How it was built" + "Verification" sections), `.github/AI_WORKFLOW.md` (companion to README's "How it was built"), `.coderabbit.yaml` (per-path conventions encoded). Codecov upload added to CI; README gains CI/CodeQL/codecov badges. Twelve community skills *deliberately not installed* (frontend tools when no frontend, ci-cd-pipeline-builder when CI works, etc.) — curation > breadth. -- **OrderService integration tests now actually pass in CI** (this PR, in flight) — the suite was added 2026-05-15 (`76b2f2f`) but never observed green in CI; every BUILD_AND_TEST run from then through 2026-05-22 was cancelled at the runner's max because the test job hung. Two root causes: +- **OrderService integration tests now actually pass in CI** (PR #17 merged 2026-05-23) — the suite was added 2026-05-15 (`76b2f2f`) but never observed green in CI; every BUILD_AND_TEST run from then through 2026-05-22 was cancelled at the runner's max because the test job hung. First CI green: 2026-05-23 commit `12a5071e` — Catalog integration in 58s, Order integration in 40s. Two root causes: - **`.AutoProvision()` against a fake ASB endpoint hangs host startup** — `opts.UseAzureServiceBus(connectionString).AutoProvision()` is a transport-bootstrapper hook that runs *before* `DisableAllExternalWolverineTransports()` from a test factory's `ConfigureTestServices` takes effect. The fake `sb://fake.servicebus.windows.net/...` connection string would retry with backoff until the runner killed the job. Fix: gate `.AutoProvision()` on a `Wolverine:AutoProvision` config flag (defaults `true` so dev/prod are unchanged); test factory sets it to `false`. Applied across all 4 services with ASB (Order, Payment, Shipping, Notification) — preventive variant fix since only OrderService has integration tests today. - **`OrderApiFactory.DisposeAsync` stopped the SQL Server container before the host shut down** — Wolverine's `DurableReceiver` background service kept polling the outbox tables during the teardown window and crashed the test host on connection refused. Fix: `await base.DisposeAsync()` before `_sqlServer.DisposeAsync()`. - - **Codecov upload added to the integration-tests job** (with `--collect "XPlat Code Coverage"` on both `dotnet test` steps + a flagged `integration` upload). Codecov badge was 18.28% post-merge of PR #15 because only unit-test coverage was being uploaded; with this in place the badge should reflect the ~58% aggregate baseline. + - **Codecov upload added to the integration-tests job** (with `--collect "XPlat Code Coverage"` on both `dotnet test` steps + a flagged `integration` upload). Codecov main-branch badge jumped from **18.28% → 56.14%** post-merge — close to (slightly under) the 58.5% baseline cited above. Small gap is Coverlet attribution differences between reportgenerator's own aggregation and Codecov's per-flag combine. - Discovered + fixed using the new `systematic-debugging` skill (Phase 1-4 discipline) + `variant-analysis` skill (one bug, sibling files) from PR #15. First real use of the new tooling. - **Demo deployment LIVE on Fly.io** — `CatalogService.Api` deployed to https://catalog-api-demo.fly.dev with Scalar UI at `/scalar/v1`. Single Fly Machine in `lax`, auto-stops when idle (~$5/mo with $25 prepaid credit cap, no card on file = hard ceiling). Scaffolding: `DemoMode` config flag in [Program.cs](../CatalogService/CatalogService.Api/Program.cs) gates Scalar + OpenAPI + skip-HTTPS-redirect + migrate-on-startup in non-Development environments; Redis registration is conditional on a `cache` connection string so HybridCache degrades to L1-only. New [Dockerfile.catalog](../Dockerfile.catalog), [.dockerignore](../.dockerignore), [fly.toml](../fly.toml), [.github/workflows/deploy-catalog-demo-fly.yml](../.github/workflows/deploy-catalog-demo-fly.yml) (Fly path — primary), and [.github/workflows/deploy-catalog-demo.yml](../.github/workflows/deploy-catalog-demo.yml) (AWS App Runner — alternative). Setup recipe in [docs/demo-deployment.md](demo-deployment.md); narrative + gotchas + EF migration decisions in [docs/demo-deployment-story.md](demo-deployment-story.md). - **Smoke-test debugging arc** (commits `1cb5ea8` through `8019891`) — surfaced and captured five Aspire/Wolverine 13.x gotchas: SDK/package version alignment, globally-unique subscription names, `RunAsEmulator()` for Service Bus, `IsPublishMode` gate on App Insights, `WaitFor()` for service-startup gating, Wolverine middleware requires instance methods. Each fix paired with a CLAUDE.md rule update per the Debugging Discipline. @@ -59,15 +59,15 @@ These two commits collectively add: - **OrderService PlaceOrder parallelization** — N sequential gRPC round-trips collapsed into two parallel phases: `Task.WhenAll` validation, then `Task.WhenAll` stock reservation. Phase split is intentional — a validation failure no longer leaves stranded reservations (previously, line 3 failing meant lines 1-2 had already reserved). gRPC calls don't touch OrderService's DbContext, so CLAUDE.md's "DbContext is not thread-safe" rule still holds. Batch-RPC shape captured as a deferred follow-up in Open Issues. - **`WolverineEventPublisher` cancellation propagation** — all three services (`OrderService`, `PaymentService`, `ShippingService`) now `ct.ThrowIfCancellationRequested()` before calling `bus.PublishAsync`. Wolverine 5.36's `IMessageBus.PublishAsync` has no CancellationToken overload, so this is the cleanest way to refuse outbox staging for an already-cancelled request. - **PaymentRecoveryJob — stuck-Pending sweeper.** New `BackgroundService` that wakes every minute, acquires the `payments-recovery` distributed lock (SQL Server `sp_getapplock` via [DistributedLock.SqlServer](https://github.com/madelson/DistributedLock)), finds Payments stuck Pending past the 5-min stale threshold, and marks each Failed + publishes `PaymentFailedEvent`. RowVersion is the per-row backstop if multiple sweepers race past the lock. `TimeProvider` injected for testability. `BuyerId` denormalized onto `Payment` via migration `20260521210000_AddBuyerIdToPayment` so the event can carry it. 7 unit tests via `FakeTimeProvider`. Canonical use case for the [distributed-lock pattern documented in project-decisions §22](project-decisions.md#22-dapr--considered-not-adopted-and-distributed-locks). -- **EF Core review + two follow-up fixes** (this session, uncommitted) — full audit against the `dotnet-performance` skill rubric: concurrency tokens on every aggregate (`xmin` for Postgres, `RowVersion` for SQL Server), `AsNoTracking`/projection on read paths, no sync-over-async anywhere in production code, `CancellationToken` propagation universal, outbox atomicity verified for Wolverine handler paths via `AutoApplyTransactions` + `UseEntityFrameworkCoreTransactions`. Two real findings closed: +- **EF Core review + two follow-up fixes** (commit `825d6bd` — PaymentRecoveryJob atomicity; ProductRepository identity-resolution change landed alongside) — full audit against the `dotnet-performance` skill rubric: concurrency tokens on every aggregate (`xmin` for Postgres, `RowVersion` for SQL Server), `AsNoTracking`/projection on read paths, no sync-over-async anywhere in production code, `CancellationToken` propagation universal, outbox atomicity verified for Wolverine handler paths via `AutoApplyTransactions` + `UseEntityFrameworkCoreTransactions`. Two real findings closed: - **CatalogService ProductRepository: `AsNoTracking()` + `Include(p => p.Category)` → `AsNoTrackingWithIdentityResolution()`.** With multiple products sharing a category, plain `AsNoTracking` + `Include` duplicates the Category entity per row (the canonical EF trap). The identity-resolution variant keeps reference equality without re-enabling change tracking. Applied to `GetAllAsync` / `GetByCategoryAsync` / `SearchAsync` in [ProductRepository.cs](../CatalogService/CatalogService.Infrastructure/Repositories/ProductRepository.cs). - **PaymentRecoveryJob outbox atomicity.** The sweeper runs outside Wolverine's handler pipeline, so `AutoApplyTransactions` doesn't wrap it — `repository.UpdateAsync(payment)` (commits Failed) followed by `eventPublisher.PublishAsync(...)` had a gap where a process crash between the two would leave the Payment Failed in-DB but the event never enqueued, stalling the saga. Fixed by adding `IPaymentRepository.ExecuteInTransactionAsync(work, ct)` (wraps `Database.BeginTransactionAsync`); the sweeper now invokes the mark+publish inside the transaction. Wolverine's `UseEntityFrameworkCoreTransactions` bridges the outbox row write into the same transaction, so entity write + event stage commit atomically. Unit tests get a pass-through stub for the new repo method. ### Build / test state - `dotnet build` — clean, 0 warnings, 0 errors. - `dotnet test` — 134/134 unit tests pass. -- **Integration tests** — two slices live now, *both verified green*. **Catalog** (`tests/CatalogService.Tests.Integration`, 4 tests, Postgres + Redis): migrations apply, HybridCache caches + invalidates, `xmin` token fires. **Order** (`tests/OrderService.Tests.Integration`, 4 tests, SQL Server + stubbed Wolverine transport): Wolverine outbox + saga handlers verified — `OrderPlacedEvent` flows through the durable pipeline, `PaymentCompletedEvent` consumer transitions the Order through real EF, idempotency guards work, `RowVersion` token fires. Local run 2026-05-23: 4/4 pass in 16.2s with clean shutdown. CI confirmation pending the fix PR merge (see Recently Landed). **Cross-service choreography over the real Azure Service Bus wire** is still uncovered — that's the deferred ASB-emulator slice. -- **Coverage** — Codecov badge currently reflects unit tests only. Integration-coverage upload is now configured in `.github/workflows/ci.yml` (both `dotnet test` steps in the integration-tests job emit Cobertura XML; both flagged uploads land in Codecov). The badge + dashboard will reflect the combined aggregate (~58% line / 57% branch per reportgenerator's prior measurement) after the next successful CI run on main with the integration-tests job green. +- **Integration tests** — two slices live, both green in CI and locally. **Catalog** (`tests/CatalogService.Tests.Integration`, 4 tests, Postgres + Redis): migrations apply, HybridCache caches + invalidates, `xmin` token fires. **Order** (`tests/OrderService.Tests.Integration`, 4 tests, SQL Server + stubbed Wolverine transport): Wolverine outbox + saga handlers verified — `OrderPlacedEvent` flows through the durable pipeline, `PaymentCompletedEvent` consumer transitions the Order through real EF, idempotency guards work, `RowVersion` token fires. CI duration: Catalog 58s, Order 40s. **Cross-service choreography over the real Azure Service Bus wire** is still uncovered — that's the deferred ASB-emulator slice. +- **Coverage** — Codecov main-branch badge at **56.14%** (combined unit + integration). Close to the 58.5% baseline cited above (small gap is Coverlet attribution differences between reportgenerator's aggregation and Codecov's per-flag combine — not worth optimizing). - **Performance baselines not measured yet** — harness exists; no recorded baseline numbers. --- diff --git a/docs/dev-loop.excalidraw b/docs/dev-loop.excalidraw new file mode 100644 index 00000000..a6cd18ef --- /dev/null +++ b/docs/dev-loop.excalidraw @@ -0,0 +1,652 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "text", + "id": "title", + "x": 40, + "y": 40, + "width": 700, + "height": 38, + "text": "Dev loop & tooling", + "originalText": "Dev loop & tooling", + "fontSize": 32, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "strokeColor": "#1e40af", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100001, + "version": 2, + "versionNonce": 1627606200, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": null, + "lineHeight": 1.25, + "index": "a0", + "frameId": null, + "roundness": null, + "updated": 1779557774292, + "autoResize": true + }, + { + "type": "text", + "id": "subtitle", + "x": 40, + "y": 86, + "width": 1500, + "height": 25, + "text": "Five stages from edit to runtime. Tools that fire autonomously are filled; tools invoked manually are outlined.", + "originalText": "Five stages from edit to runtime. Tools that fire autonomously are filled; tools invoked manually are outlined.", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100003, + "version": 2, + "versionNonce": 489618120, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": null, + "lineHeight": 1.25, + "index": "a1", + "frameId": null, + "roundness": null, + "updated": 1779557774292, + "autoResize": true + }, + { + "type": "rectangle", + "id": "stage1_box", + "x": 40, + "y": 160, + "width": 320, + "height": 70, + "strokeColor": "#6d28d9", + "backgroundColor": "#ddd6fe", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100010, + "version": 2, + "versionNonce": 1298724280, + "isDeleted": false, + "groupIds": [], + "boundElements": [ + { + "id": "stage1_label", + "type": "text" + } + ], + "link": null, + "locked": false, + "roundness": { + "type": 3 + }, + "index": "a2", + "frameId": null, + "updated": 1779557774292 + }, + { + "type": "text", + "id": "stage1_label", + "x": 110.3515625, + "y": 172.5, + "width": 179.296875, + "height": 45, + "text": "1. Edit-time\nIDE + Claude Code", + "originalText": "1. Edit-time\nIDE + Claude Code", + "fontSize": 18, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "strokeColor": "#6d28d9", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100012, + "version": 4, + "versionNonce": 683975864, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": "stage1_box", + "lineHeight": 1.25, + "index": "a3", + "frameId": null, + "roundness": null, + "updated": 1779557795085, + "autoResize": true + }, + { + "type": "rectangle", + "id": "stage2_box", + "x": 390, + "y": 160, + "width": 320, + "height": 70, + "strokeColor": "#1e3a5f", + "backgroundColor": "#60a5fa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100020, + "version": 2, + "versionNonce": 1362198200, + "isDeleted": false, + "groupIds": [], + "boundElements": [ + { + "id": "stage2_label", + "type": "text" + } + ], + "link": null, + "locked": false, + "roundness": { + "type": 3 + }, + "index": "a4", + "frameId": null, + "updated": 1779557774292 + }, + { + "type": "text", + "id": "stage2_label", + "x": 481.4453125, + "y": 172.5, + "width": 137.109375, + "height": 45, + "text": "2. Build-time\ndotnet build", + "originalText": "2. Build-time\ndotnet build", + "fontSize": 18, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100022, + "version": 4, + "versionNonce": 1320157128, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": "stage2_box", + "lineHeight": 1.25, + "index": "a5", + "frameId": null, + "roundness": null, + "updated": 1779557793085, + "autoResize": true + }, + { + "type": "rectangle", + "id": "stage3_box", + "x": 740, + "y": 160, + "width": 320, + "height": 70, + "strokeColor": "#1e3a5f", + "backgroundColor": "#93c5fd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100030, + "version": 2, + "versionNonce": 1023469496, + "isDeleted": false, + "groupIds": [], + "boundElements": [ + { + "id": "stage3_label", + "type": "text" + } + ], + "link": null, + "locked": false, + "roundness": { + "type": 3 + }, + "index": "a6", + "frameId": null, + "updated": 1779557774292 + }, + { + "type": "text", + "id": "stage3_label", + "x": 760, + "y": 178, + "width": 280, + "height": 34, + "text": "3. Test-time\ndotnet test", + "originalText": "3. Test-time\ndotnet test", + "fontSize": 18, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "strokeColor": "#1e3a5f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100032, + "version": 2, + "versionNonce": 1034719176, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": "stage3_box", + "lineHeight": 1.25, + "index": "a7", + "frameId": null, + "roundness": null, + "updated": 1779557774292, + "autoResize": true + }, + { + "type": "rectangle", + "id": "stage4_box", + "x": 1090, + "y": 160, + "width": 320, + "height": 70, + "strokeColor": "#b45309", + "backgroundColor": "#fef3c7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100040, + "version": 2, + "versionNonce": 754659512, + "isDeleted": false, + "groupIds": [], + "boundElements": [ + { + "id": "stage4_label", + "type": "text" + } + ], + "link": null, + "locked": false, + "roundness": { + "type": 3 + }, + "index": "a8", + "frameId": null, + "updated": 1779557774292 + }, + { + "type": "text", + "id": "stage4_label", + "x": 1112.890625, + "y": 172.5, + "width": 274.21875, + "height": 45, + "text": "4. PR-time\nGitHub Actions + reviewers", + "originalText": "4. PR-time\nGitHub Actions + reviewers", + "fontSize": 18, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "strokeColor": "#b45309", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100042, + "version": 4, + "versionNonce": 667568072, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": "stage4_box", + "lineHeight": 1.25, + "index": "a9", + "frameId": null, + "roundness": null, + "updated": 1779557787321, + "autoResize": true + }, + { + "type": "rectangle", + "id": "stage5_box", + "x": 1440, + "y": 160, + "width": 320, + "height": 70, + "strokeColor": "#047857", + "backgroundColor": "#a7f3d0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100050, + "version": 2, + "versionNonce": 415521208, + "isDeleted": false, + "groupIds": [], + "boundElements": [ + { + "id": "stage5_label", + "type": "text" + } + ], + "link": null, + "locked": false, + "roundness": { + "type": 3 + }, + "index": "aA", + "frameId": null, + "updated": 1779557774292 + }, + { + "type": "text", + "id": "stage5_label", + "x": 1505.078125, + "y": 172.5, + "width": 189.84375, + "height": 45, + "text": "5. Merge / Runtime\nAspire + Fly.io", + "originalText": "5. Merge / Runtime\nAspire + Fly.io", + "fontSize": 18, + "fontFamily": 3, + "textAlign": "center", + "verticalAlign": "middle", + "strokeColor": "#047857", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100052, + "version": 4, + "versionNonce": 1478763464, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "containerId": "stage5_box", + "lineHeight": 1.25, + "index": "aB", + "frameId": null, + "roundness": null, + "updated": 1779557789868, + "autoResize": true + }, + { + "type": "arrow", + "id": "stage_arrow_1_2", + "x": 362, + "y": 195, + "width": 26, + "height": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100060, + "version": 2, + "versionNonce": 1669358264, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 26, + 0 + ] + ], + "startBinding": { + "elementId": "stage1_box", + "focus": 0, + "gap": 2 + }, + "endBinding": { + "elementId": "stage2_box", + "focus": 0, + "gap": 2 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "lastCommittedPoint": null, + "index": "aC", + "frameId": null, + "roundness": null, + "updated": 1779557774292 + }, + { + "type": "arrow", + "id": "stage_arrow_2_3", + "x": 712, + "y": 195, + "width": 26, + "height": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100062, + "version": 2, + "versionNonce": 1563837640, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 26, + 0 + ] + ], + "startBinding": { + "elementId": "stage2_box", + "focus": 0, + "gap": 2 + }, + "endBinding": { + "elementId": "stage3_box", + "focus": 0, + "gap": 2 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "lastCommittedPoint": null, + "index": "aD", + "frameId": null, + "roundness": null, + "updated": 1779557774292 + }, + { + "type": "arrow", + "id": "stage_arrow_3_4", + "x": 1062, + "y": 195, + "width": 26, + "height": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100064, + "version": 2, + "versionNonce": 1809009592, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 26, + 0 + ] + ], + "startBinding": { + "elementId": "stage3_box", + "focus": 0, + "gap": 2 + }, + "endBinding": { + "elementId": "stage4_box", + "focus": 0, + "gap": 2 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "lastCommittedPoint": null, + "index": "aE", + "frameId": null, + "roundness": null, + "updated": 1779557774292 + }, + { + "type": "arrow", + "id": "stage_arrow_4_5", + "x": 1412, + "y": 195, + "width": 26, + "height": 0, + "strokeColor": "#64748b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "seed": 100066, + "version": 2, + "versionNonce": 1721920456, + "isDeleted": false, + "groupIds": [], + "boundElements": [], + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 26, + 0 + ] + ], + "startBinding": { + "elementId": "stage4_box", + "focus": 0, + "gap": 2 + }, + "endBinding": { + "elementId": "stage5_box", + "focus": 0, + "gap": 2 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "lastCommittedPoint": null, + "index": "aF", + "frameId": null, + "roundness": null, + "updated": 1779557774292 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/dev-loop.md b/docs/dev-loop.md new file mode 100644 index 00000000..ca2575b2 --- /dev/null +++ b/docs/dev-loop.md @@ -0,0 +1,233 @@ +# Development Loop — Tooling & Configuration + +A pragmatic inventory of every tool, hook, agent, and config that shapes how +code lands in this repo, organized by *when* in the dev loop each one fires. + +Visual companion: [dev-loop.svg](dev-loop.svg) (source: [dev-loop.excalidraw](dev-loop.excalidraw)). + +The bar this document is held to: **describe what's actually in place** (with +file paths so you can verify), **call out gaps honestly**, and **for each gap, +propose a pragmatic solution sized to the actual problem** — no +build-a-whole-new-system suggestions. + +--- + +## Stage 1 — Edit-time (IDE + Claude Code) + +This is where most code originates. The tooling here shapes proposed edits +*before* they land in a file. + +### Canonical rules +| File | Role | +|---|---| +| [CLAUDE.md](../CLAUDE.md) | 25 KB of opinionated rules — SOLID/DDD/VSA-vs-Clean, performance, security, conventions, debugging discipline. Loaded into every Claude Code session. | +| [.editorconfig](../.editorconfig) | Naming + formatting enforced by Roslyn at build time. | +| [Directory.Build.props](../Directory.Build.props) | Shared build settings (TreatWarningsAsErrors, target framework, analyzers). | +| [Directory.Packages.props](../Directory.Packages.props) | Central package management — versions live here, csproj files have no version attributes. | +| [BannedSymbols.txt](../BannedSymbols.txt) | Banned APIs (`Task.WaitAll`, `Parallel.For`, `Thread.Sleep`, etc.) enforced by `BannedApiAnalyzers`. | + +### Hooks ([.claude/scripts/](../.claude/scripts/)) +| Hook | Event | What it does | +|---|---|---| +| [block-sync-over-async.sh](../.claude/scripts/block-sync-over-async.sh) | `PreToolUse` (Edit\|Write on .cs) | Rejects `.Result` / `.Wait()` / `.GetAwaiter().GetResult()` *in proposed edits*. Build-time net (BannedSymbols.txt) catches the same patterns later — this hook catches them earlier so the bad diff never lands. | +| [inject-status.sh](../.claude/scripts/inject-status.sh) | `SessionStart` | Injects top of STATUS.md + current branch + last commit so sessions don't start cold. | +| [check-claude-md-refs.sh](../.claude/scripts/check-claude-md-refs.sh) | `PostToolUse` (Edit\|Write on CLAUDE.md) | When CLAUDE.md changes, lists every file containing the `See CLAUDE.md` paraphrase marker so drift can be reviewed. | + +### Slash commands ([.claude/commands/](../.claude/commands/)) +| Command | Purpose | +|---|---| +| [/new-feature-slice](../.claude/commands/new-feature-slice.md) | Scaffolds a VSA feature slice matching the [OrderService/Features/PlaceOrder.cs](../OrderService/Features/PlaceOrder.cs) canonical shape. Refuses for CatalogService (Clean Architecture). | +| [/sync-status](../.claude/commands/sync-status.md) | Refreshes STATUS.md from `git log` + open issues. | +| [/check-rules](../.claude/commands/check-rules.md) | Audits every `See CLAUDE.md` paraphrase against the canonical rule. | + +### Agents ([.claude/agents/](../.claude/agents/)) +| Agent | Purpose | +|---|---| +| [architecture-reviewer](../.claude/agents/architecture-reviewer.md) | Loads CLAUDE.md + [architecture-map.md](../.claude/architecture-map.md), evaluates a target against SOLID/DDD/VSA-vs-Clean/Performance rules. Reports only — no edits. | + +### Skills ([.claude/skills/](../.claude/skills/)) +| Skill | Source | When it fires | +|---|---|---| +| dotnet-performance | this repo | Writing handlers, queries, repositories, middleware, migrations | +| excalidraw-diagram | this repo | Generating diagrams (this doc's diagram, in fact) | +| skill-security-auditor | [alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills) | Pre-install security gate. Audits any skill before install. | +| verification-before-completion | [obra/superpowers](https://github.com/obra/superpowers) | About to claim "done/fixed/passing" — forces evidence. | +| systematic-debugging | [obra/superpowers](https://github.com/obra/superpowers) | Any bug, test failure, unexpected behavior. Four-phase root cause discipline. | +| variant-analysis | [trailofbits/skills](https://github.com/trailofbits/skills) | One bug found — search every similar pattern across the codebase. | +| test-driven-development | [obra/superpowers](https://github.com/obra/superpowers) | Implementing a feature or bugfix — RED-GREEN-REFACTOR. | +| using-git-worktrees | [obra/superpowers](https://github.com/obra/superpowers) | Feature work that needs workspace isolation. | +| writing-plans + executing-plans | [obra/superpowers](https://github.com/obra/superpowers) | Spec-driven multi-step task with review checkpoints. | + +### Architecture map +[.claude/architecture-map.md](../.claude/architecture-map.md) — code-graph for AI + humans. Services, shapes (Clean vs VSA), event flow, ports, aggregates, concurrency tokens. + +### Secondary AI reviewer +[GitHub Copilot](https://github.com/features/copilot) (GPT-5) in-editor for second-opinion diff review. Conventions encoded in [.github/copilot-instructions.md](../.github/copilot-instructions.md). The principle: disagreement between Claude and Copilot is a signal to dig deeper, not pick the louder voice. + +--- + +## Stage 2 — Build-time (`dotnet build`) + +Static analysis that runs as part of every build. `TreatWarningsAsErrors` is on; zero warnings allowed. + +| Analyzer | Catches | +|---|---| +| **Meziantou.Analyzer** | C# best practices — design, performance, security, usage. ~200 rules. | +| **SonarAnalyzer.CSharp** | Code smells, bugs, vulnerabilities — same engine as SonarQube/SonarCloud. | +| **Roslynator.Analyzers** | Refactoring + style suggestions. | +| **BannedApiAnalyzers** + [BannedSymbols.txt](../BannedSymbols.txt) | Forbidden concurrency hazards (Task.WaitAll, Parallel.For, Thread.Sleep, etc.) with custom replacement guidance. | +| **C# nullability** | NRTs enabled — null-state analysis catches most NREs at compile. | +| Standard .NET 10 compiler warnings | Treated as errors. | + +--- + +## Stage 3 — Test-time (`dotnet test`) + +| Tool | Purpose | +|---|---| +| **xunit** | Test runner. | +| **AwesomeAssertions** | Fluent assertion library (drop-in fork of FluentAssertions 8 — migrated off ahead of FA's paid license). | +| **NSubstitute** | Mocking for unit tests. | +| **Microsoft.AspNetCore.Mvc.Testing** + `WebApplicationFactory` | In-process API hosting for integration tests. | +| **Testcontainers** | Real DB / Redis / messaging via Docker for integration tests. macOS uses `~/.docker/run/docker.sock`; CI uses standard path. | +| **Coverlet** (via `--collect "XPlat Code Coverage"`) | Cobertura XML coverage measurement, per-test-project. | +| **reportgenerator** | Aggregates per-project Cobertura into a single markdown summary in the CI job summary. | +| **BenchmarkDotNet** | Microbenchmarks at [benchmarks/NextAurora.Benchmarks](../benchmarks/NextAurora.Benchmarks). | +| **k6** | Load smoke at [scripts/k6/smoke.js](../scripts/k6/smoke.js). | + +Two integration slices today: **CatalogService** (Postgres + Redis) and **OrderService** (SQL Server + stubbed Wolverine transport). + +--- + +## Stage 4 — PR-time (GitHub Actions) + +| Workflow | Purpose | +|---|---| +| [.github/workflows/ci.yml](../.github/workflows/ci.yml) | Build + unit tests (with Codecov upload) + concurrency-audit grep + integration tests (with Codecov upload). NuGet cache. `concurrency: cancel-in-progress` on the workflow. | +| [.github/workflows/codeql.yml](../.github/workflows/codeql.yml) | CodeQL SAST. `security-and-quality` query set. Weekly + on PR. | +| [.github/dependabot.yml](../.github/dependabot.yml) | NuGet weekly (grouped per ecosystem), GitHub Actions monthly. | +| [.github/workflows/deploy-catalog-demo-fly.yml](../.github/workflows/deploy-catalog-demo-fly.yml) | Deploy CatalogService.Api to Fly.io (primary path). | +| [.github/workflows/deploy-catalog-demo.yml](../.github/workflows/deploy-catalog-demo.yml) | AWS App Runner alternative (scaffolded, not actively used). | + +### PR-side configuration +| File | Role | +|---|---| +| [.github/PULL_REQUEST_TEMPLATE.md](../.github/PULL_REQUEST_TEMPLATE.md) | "How it was built" (AI vs hand-written) + Verification sections to keep PR claims honest. | +| [.github/AI_WORKFLOW.md](../.github/AI_WORKFLOW.md) | Companion to README's "How it was built" — exact tools, guardrails, what's deliberately NOT used. | +| [.github/copilot-instructions.md](../.github/copilot-instructions.md) | Copilot-side conventions. | +| [.coderabbit.yaml](../.coderabbit.yaml) | CodeRabbit per-path instructions encoding THIS project's conventions (VSA vs Clean, `MapV1ApiGroup`, async/CancellationToken, EF migrations immutable, etc.). Requires CodeRabbit GitHub App installed. | + +### Reviewers +| Reviewer | Strengths | Limits | +|---|---|---| +| **CodeRabbit** | LLM-based, reads diffs, picks up cross-file consistency, missing tests, naming drift. Project-specific via per-path instructions. | Not deterministic — same diff can produce different findings. Profile "assertive" surfaces more findings than "chill". | +| **Codecov** | Coverage trend, per-file deltas, PR-level coverage report. Free OSS tier. | Doesn't *gate* PRs without explicit threshold config (currently no gate — see Gaps). | +| **CodeQL** | Static security analysis. Hosted by GitHub. | C# rule set is broad but generic — not project-specific. | +| **dorny/test-reporter** | Surfaces TRX test results as a PR check run instead of buried in job logs. | Just reporting; no analysis. | +| **architecture-reviewer agent** | Project-specific, applies CLAUDE.md rules. Invoked manually. | Doesn't auto-fire on PR — must be triggered. | + +--- + +## Stage 5 — Merge / Runtime + +| Tool | Role | +|---|---| +| **.NET Aspire** | Local dev orchestration. `dotnet run --project NextAurora.AppHost` brings up all services + Postgres + SQL Server + Service Bus emulator + Redis + Keycloak in one command. Aspire dashboard at http://localhost:18888. | +| **OpenTelemetry** | Traces + metrics + logs throughout. Aspire ingests in dev; Application Insights ingests in prod. | +| **Wolverine** | In-process message bus + transactional outbox. Adapter for Azure Service Bus. | +| **Scalar UI** | Interactive API docs at `/scalar/v1` per service (dev-only). | +| **Fly.io** | CatalogService demo at https://catalog-api-demo.fly.dev. Single Machine, auto-stops when idle. | +| **CorrelationId middleware** (in [NextAurora.ServiceDefaults](../NextAurora.ServiceDefaults/)) | Correlation/User/Session ID propagation across HTTP + Service Bus boundaries. | + +--- + +## Gaps — and the pragmatic solution for each + +The gaps below are real. Each one is sized for how much the *actual* problem warrants — not how much could theoretically be done. + +### Gap 1 — Cross-service E2E over the real Azure Service Bus wire is not tested + +**What's missing:** Integration tests today use a stubbed Wolverine transport. The actual `OrderPlacedEvent` → ASB → PaymentService consumer round-trip is uncovered. + +**Pragmatic solution:** Defer until needed. The stubbed-transport tests cover the load-bearing correctness (handler logic, outbox staging, EF + concurrency tokens); the wire itself mostly exercises Microsoft's ASB emulator + Wolverine's adapter — the fragile last mile, not the architecture. When this slice does land, gate it as a **manual nightly job** (`workflow_dispatch:` or `schedule:` once a day), not every PR — the ASB emulator container wants an MSSQL sidecar and adds ~3 minutes to every run. Not worth that tax per-PR. + +### Gap 2 — No production performance baselines + +**What's missing:** BenchmarkDotNet + k6 harness exists but has never run under realistic concurrent traffic. We can't tell the difference between "fast enough" and "lucky so far." + +**Pragmatic solution:** Pick exactly two endpoints to baseline — `GET /api/v1/products/{id}` (read-heavy hot path) and `POST /api/v1/orders` (the saga entry point). Run a k6 profile at 100 concurrent users, capture P50/P95/P99 + GC-pause distribution (`dotnet-counters` for `System.Runtime`) + HybridCache hit ratio. Commit the numbers to `docs/perf-baselines.md` (file not yet created) as the baseline. Re-measure quarterly or on perf-sensitive PRs. Don't try to baseline everything — pick the two highest-traffic endpoints, baseline once, move on. + +### Gap 3 — `.claude/settings.json` accumulates session cruft + +**What's missing:** Claude Code's auto-permission-grant flow saves narrow per-command allow entries during active sessions. Over a busy session, settings.json bloats with 30+ one-off entries. + +**Pragmatic solution:** Don't build a hook. Just `git restore .claude/settings.json` periodically — every commit, basically. The durable wildcard entries (`Bash(dotnet *)`, `Bash(git *)`, etc.) are stable; the one-offs are noise. If this becomes too annoying, add a Stop hook (8-line bash script) that strips any allow entry not in a curated whitelist on session end. **Don't write the hook yet** — the manual restore is fine until the friction is measurable. + +### Gap 4 — GitHub Actions are version-pinned (`@vN`), not SHA-pinned + +**What's missing:** Supply-chain hardening best practice is to pin actions to immutable commit SHAs so a maintainer can't change what runs by re-tagging. + +**Pragmatic solution:** Stick with `@vN` tags + Dependabot Actions weekly updates as the layered defense (a tag move would be detected by Dependabot within ~24h). If higher assurance is needed, run [pin-github-action](https://github.com/mheap/pin-github-action) once to SHA-pin everything in a single hardening PR — *all six actions* (`actions/checkout`, `actions/setup-dotnet`, `actions/cache`, `dorny/test-reporter`, `github/codeql-action/*`, `codecov/codecov-action`), not one at a time. Inconsistent pinning is the worst of both worlds. + +### Gap 5 — No coverage gate + +**What's missing:** Codecov shows the badge + trend, but doesn't fail PRs when coverage drops. + +**Pragmatic solution:** Add a `codecov.yml` at repo root (file not yet created) with `coverage.status.project: target: auto, threshold: 1%`. That lets normal PRs through but fails ones that drop coverage by >1%. Don't set absolute thresholds (e.g. "must be 80%") — they create perverse incentives (delete uncovered code instead of testing it). Relative threshold = "don't make it worse." + +### Gap 6 — AppHost smoke run is manual + +**What's missing:** [scripts/smoke-test.sh](../scripts/smoke-test.sh) verifies service liveness, versioning, auth flow, order placement — but only runs when someone remembers to invoke it. + +**Pragmatic solution:** Add a `workflow_dispatch:` job that runs against the Fly demo (or spins up Aspire in a self-hosted runner — heavy). Skip per-PR; trigger nightly via `schedule:` cron OR manually when investigating a deployment regression. **Not worth running on every PR** — Aspire boot is 60+ seconds even with cache, and most PRs don't change the smoke surface. + +### Gap 7 — No secret scanning beyond CodeQL + +**What's missing:** CodeQL covers SAST but doesn't dedicated-scan for hardcoded secrets, leaked keys, or known-vulnerable dependency CVEs beyond what Dependabot catches. + +**Pragmatic solution:** Add one GitHub Action: [`gitleaks/gitleaks-action@v2`](https://github.com/gitleaks/gitleaks-action). Five-line workflow, free for public repos, scans every PR for secret-looking patterns. Pair with a quarterly run of `dotnet list package --vulnerable` (5-line shell script) for CVE deps. Both are low-effort additions. + +### Gap 8 — Production migration deploy step not automated + +**What's missing:** `MigrateDatabaseAsync` only runs in `Development` environment. Production migrations require manual `dotnet ef database update`. + +**Pragmatic solution:** This is the *right* design — auto-migrating on prod startup is dangerous (a bad migration takes down all replicas simultaneously). Keep the manual run, but add a **separate `deploy-migrate` GitHub Actions job** that runs `dotnet ef database update --no-build` against the production connection string, **gated by a manual approval environment** (`environment: production-migration` with required reviewers). Solves the automation gap without losing the safety. + +### Gap 9 — CodeRabbit + architecture-reviewer agent feel redundant + +**What's missing:** Both review code on PRs. The overlap is real. + +**Pragmatic solution:** Keep both — they catch different things. CodeRabbit is rules-based (cross-file consistency, naming drift, missing tests, generic .NET hygiene). The architecture-reviewer agent is rule-*application* (does this slice respect THIS project's SOLID/DDD/VSA-vs-Clean rules using CLAUDE.md as canon?). Not redundant — complementary. The signal you're looking for is: CodeRabbit fires automatically on every PR; the architecture-reviewer is invoked manually for *non-trivial architectural changes*. Different cadence, different lens. + +### Gap 10 — No "this PR is AI-generated" tagging in commits + +**What's missing:** The PR template asks the author to declare AI involvement, but commits don't carry it (beyond Claude Code's `Co-Authored-By:` line, which not every contributor uses). + +**Pragmatic solution:** Don't add a tag. The PR template covers the disclosure layer; commit-level tagging would create noise + ask contributors to remember another convention. The Co-Authored-By line that Claude Code adds automatically is sufficient signal where it's used. + +--- + +## What we deliberately don't use + +These are tools considered and skipped, for the record. (See [.github/AI_WORKFLOW.md "What I don't use AI for"](../.github/AI_WORKFLOW.md) for the curation rationale.) + +| Tool | Why skipped | +|---|---| +| **SonarCloud** (hosted dashboard) | Overlap with existing SonarAnalyzer.CSharp at build time. Codecov badge gives the trend signal; SonarCloud would add a dashboard without much new detection. | +| **DependenSee** (project dep graph SVG) | Considered for the architecture-map; not implemented yet. The architecture map serves the same purpose for AI consumption. May add later if a human-facing diagram becomes useful. | +| **SonarQube** (self-hosted) | Self-hosting infrastructure overhead doesn't pay back at this project size. | +| **Frontend testing tools** (Playwright, etc.) | Storefront + SellerPortal are static-file scaffolds — no frontend to test. | +| **MCP servers** | Not building an MCP server. | +| **CI/CD pipeline generator skills** | Existing CI works; adding a generator is anti-pragmatic. | +| **Differential-review skill** (trailofbits) | Direct overlap with the architecture-reviewer agent + CodeRabbit. | + +--- + +## Source links + +- [CLAUDE.md](../CLAUDE.md) — canonical project rules +- [docs/STATUS.md](STATUS.md) — cross-session entry point +- [docs/architecture.md](architecture.md) — services + communication patterns +- [.claude/architecture-map.md](../.claude/architecture-map.md) — code-graph for AI + humans +- [.github/AI_WORKFLOW.md](../.github/AI_WORKFLOW.md) — the "how" of AI-assisted work +- [README.md "How it was built"](../README.md) — the surface story diff --git a/docs/dev-loop.svg b/docs/dev-loop.svg new file mode 100644 index 00000000..fbc2c5e1 --- /dev/null +++ b/docs/dev-loop.svg @@ -0,0 +1,245 @@ + + + + + + Dev loop & tooling + Five stages from edit to runtime. Bold = fires autonomously. Italic = invoked manually. + + + + 1. Edit-time + IDE + Claude Code + + + + 2. Build-time + dotnet build + + + + 3. Test-time + dotnet test + + + + 4. PR-time + GitHub Actions + reviewers + + + + 5. Merge / Runtime + Aspire + Fly.io + + + + + + + + + + + + + + + CodeRabbit / arch-reviewer findings → fix → push → re-review + + + + + + Edit-time + + Canonical rules + CLAUDE.md (25 KB) + .editorconfig + BannedSymbols.txt + + Hooks (autonomous) + PreToolUse: block-sync-over-async.sh + SessionStart: inject-status.sh + PostToolUse: check-claude-md-refs.sh + + Slash commands (manual) + /new-feature-slice + /sync-status + /check-rules + + Agents (manual) + architecture-reviewer + + Skills (auto-trigger on intent) + verification-before-completion + systematic-debugging + test-driven-development + variant-analysis + writing-plans + executing-plans + using-git-worktrees + skill-security-auditor + dotnet-performance, excalidraw-diagram + + Secondary reviewer (in-editor) + GitHub Copilot (GPT-5) + + + + + + Build-time + + Analyzers (zero warnings allowed) + Meziantou.Analyzer + SonarAnalyzer.CSharp + Roslynator.Analyzers + BannedApiAnalyzers + + BannedSymbols.txt + C# nullability (NRT) + + Build config + TreatWarningsAsErrors + Directory.Build.props + Directory.Packages.props + (central package management) + + + + + + Test-time + + Unit tests + xunit + AwesomeAssertions + NSubstitute + + Integration tests + WebApplicationFactory<Program> + Testcontainers (Docker) + Catalog: Postgres + Redis + Order: SQL Server + Wolverine + + Coverage + benchmarks + Coverlet (Cobertura XML) + reportgenerator + BenchmarkDotNet + k6 (load smoke) + + + + + + PR-time + + GitHub Actions workflows + BUILD_AND_TEST + build + unit + integration + Codecov + CodeQL (SAST) + Dependabot + NuGet weekly + Actions monthly + deploy-catalog-demo-fly.yml + + PR-side config + PULL_REQUEST_TEMPLATE.md + AI_WORKFLOW.md + copilot-instructions.md + .coderabbit.yaml + + Reviewers + CodeRabbit (LLM, autonomous) + Codecov (coverage trend) + CodeQL findings + dorny/test-reporter + architecture-reviewer agent + (manual invocation) + + + + + + Merge / Runtime + + Local dev orchestration + .NET Aspire + AppHost wires everything + Aspire dashboard (:18888) + + Messaging + data + Wolverine (outbox + saga) + Azure Service Bus emulator + Postgres + SQL Server + Redis + Keycloak (auth) + + Observability + OpenTelemetry (traces/metrics/logs) + CorrelationId / UserId / SessionId + propagated HTTP + Service Bus + Application Insights (prod) + + API surface + deploy + Scalar UI (/scalar/v1) + Fly.io (CatalogService demo) + catalog-api-demo.fly.dev + + + ━━━ flow from edit to runtime ╌╌╌ iteration loop (review findings) Bold = autonomous Italic = manual ◯ = gap with sized fix + + + + + + Gaps — and the pragmatic fix for each + Each gap is sized to the actual problem, not maximum theoretical coverage. Full rationale in dev-loop.md. + + + 1. Cross-service E2E over real Azure Service Bus wire not tested + Fix: defer. Stubbed-transport tests cover load-bearing correctness. When this lands, gate as manual nightly (workflow_dispatch:), not per-PR. + + 6. AppHost smoke run is manual + Fix: workflow_dispatch: against the Fly demo OR self-hosted Aspire. Skip per-PR; Aspire boot is 60+ seconds and most PRs don't change smoke surface. + + + 2. No production performance baselines + Fix: baseline exactly 2 endpoints (GET /products/{id}, POST /orders) at 100 concurrent. Capture P50/P95/P99 + GC + cache hit ratio. Re-measure quarterly. + + 7. No secret scanning beyond CodeQL + Fix: add gitleaks/gitleaks-action@v2 (5 lines, free for public). Pair with quarterly `dotnet list package --vulnerable`. + + + 3. .claude/settings.json accumulates session cruft + Fix: don't build a hook. `git restore .claude/settings.json` periodically. If friction grows, add an 8-line Stop hook that strips non-whitelist entries. + + 8. Production migration deploy not automated + Fix: separate deploy-migrate job with `environment: production-migration` requiring manual approval. Auto-migrating on prod startup is dangerous. + + + 4. GitHub Actions version-pinned, not SHA-pinned + Fix: stick with @vN + Dependabot Actions weekly (tag-move detected within ~24h). If higher assurance needed, run pin-github-action on ALL actions in one PR. + + 9. CodeRabbit + architecture-reviewer feel redundant + Fix: keep both — complementary. CodeRabbit = rules-based, every PR. arch-reviewer = rule-application using CLAUDE.md, manual for non-trivial architectural changes. + + + 5. No coverage gate + Fix: codecov.yml with `coverage.status.project: target: auto, threshold: 1%`. Relative gate prevents regressions without perverse incentives of absolute thresholds. + + 10. No "AI-generated" commit tagging + Fix: don't add a tag. The PR template covers the disclosure layer; Co-Authored-By line that Claude Code adds is sufficient signal where it's used. + + + Full source in docs/dev-loop.md. Source .excalidraw at docs/dev-loop.excalidraw (open in Excalidraw to edit). +