From 2085645adcd2a27c122cecc44ad33c3fcb094bd9 Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Sat, 23 May 2026 11:51:10 -0600 Subject: [PATCH 1/3] docs(dev-loop): inventory all tooling + visual workflow + honest gap analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new files in docs/: - dev-loop.md (markdown inventory) — every tool, hook, agent, skill, and config that shapes how code lands in this repo, organized by *when* in the dev loop it fires (5 stages: Edit-time, Build-time, Test-time, PR-time, Merge/Runtime). For each tool: file path so a reader can verify it actually exists. - dev-loop.svg (visual companion) — the same 5-stage flow as a left-to-right diagram with per-stage tool detail and a dotted-loop arrow showing review iteration. Hand-written SVG (not generated via Playwright — the excalidraw render script hit a 30s+ timeout loading the esm.sh ES module repeatedly in this environment). - dev-loop.excalidraw (editable source) — the highway skeleton in Excalidraw format. Open in Excalidraw web/desktop or the VS Code Excalidraw extension to extend; export as SVG to refresh dev-loop.svg. Gap analysis is the load-bearing part of the doc. Each gap (10 total — cross-service E2E, perf baselines, settings.json cruft, SHA-pinning, coverage gate, smoke-run automation, secret scanning, prod migrations, agent-vs-CodeRabbit overlap, AI-commit-tagging) is paired with a sized pragmatic fix — explicitly not "build a whole new system." The signal this doc tries to send is: knowing when to stop adding tooling is more pragmatic than maximum theoretical coverage. Co-Authored-By: Claude Opus 4.7 --- docs/dev-loop.excalidraw | 652 +++++++++++++++++++++++++++++++++++++++ docs/dev-loop.md | 233 ++++++++++++++ docs/dev-loop.svg | 245 +++++++++++++++ 3 files changed, 1130 insertions(+) create mode 100644 docs/dev-loop.excalidraw create mode 100644 docs/dev-loop.md create mode 100644 docs/dev-loop.svg 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..47351e1f --- /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](#) 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 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). + From 3ed9a3d8b1088a42e7b18981b6e5174c84aaa8db Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Sat, 23 May 2026 12:13:14 -0600 Subject: [PATCH 2/3] docs(status): freshness pass after PR #17 merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #17 (fix/orderservice-integration-test-hang) merged 2026-05-23 17:56 UTC. The STATUS.md text I added in that PR had forward-looking phrasing ("this PR, in flight", "will reflect ... after the next successful CI run", "CI confirmation pending the fix PR merge") that goes stale the moment the PR lands. Rewording to describe present state: - OrderService integration test bullet: cite the merge (PR #17) and the first-green CI timestamp + per-step durations (Catalog 58s, Order 40s). - Codecov sub-bullet: cite the measured delta (18.28% → 56.14%) instead of the expected aggregate. - Build/test state: drop "Local run / CI pending" duality — both pass now. - Coverage line: state the current badge value, not a future expectation. Also catches one older stale entry: the EF Core review + fixes bullet still said "(this session, uncommitted)" — that work landed in 825d6bd already. Replaced with the commit hash for findability. Co-Authored-By: Claude Opus 4.7 --- docs/STATUS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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. --- From b16913122e2a9f765ce6466c3aabb5962263ae3f Mon Sep 17 00:00:00 2001 From: emeraldleaf Date: Sat, 23 May 2026 16:26:01 -0600 Subject: [PATCH 3/3] docs(dev-loop): resolve placeholder [text](#) markdown links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit (PR #20) flagged two empty markdown links — perf-baselines.md and codecov.yml — both rendered as [text](#) because the referenced files don't exist yet (they're future-work suggestions in the gap analysis). MD042 (no-empty-links) violation. Fix is dropping the link syntax and calling out parenthetically that the file isn't yet created — keeps the prose readable without producing a clickable "#" link that goes nowhere. Co-Authored-By: Claude Opus 4.7 --- docs/dev-loop.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev-loop.md b/docs/dev-loop.md index 47351e1f..ca2575b2 100644 --- a/docs/dev-loop.md +++ b/docs/dev-loop.md @@ -155,7 +155,7 @@ The gaps below are real. Each one is sized for how much the *actual* problem war **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](#) 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. +**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 @@ -173,7 +173,7 @@ The gaps below are real. Each one is sized for how much the *actual* problem war **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 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." +**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