Information at endpoint and service-method boundaries, Debug in deeper helpers, Warning before expected/recoverable failures (not-found, validation, agent tool errors), Error on caught exceptions. Serilog wraps the exception handler so request-completion logs report the resolved status code rather than the raw exception. Never logs prose bodies or the Anthropic API key.
107 lines
7.5 KiB
Markdown
107 lines
7.5 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for Claude Code (claude.ai/code) in this repo.
|
|
|
|
## Project
|
|
|
|
Novelly: software for planning and writing a novel. ASP.NET Core 10, C#, TypeScript, React, .NET Aspire. Chapter outlines, character dossiers, prose drafting, an embedded Claude agent, and an MCP server over the same API.
|
|
|
|
## Structure
|
|
|
|
- `src/Novelly.Api/` — the whole back end, organised by feature. One folder per feature holds its
|
|
entity, DTOs, service and endpoints together: `Projects/`, `Characters/`, `Chapters/`, `Beats/`,
|
|
`Scenes/`, `Tags/`, `Agent/`. `Common/` holds what genuinely crosses features; `Data/` holds the
|
|
`DbContext` and EF migrations.
|
|
- `src/Novelly.AppHost/` — .NET Aspire orchestration; run this to bring up the API and the web client
|
|
- `src/Novelly.ServiceDefaults/` — shared Aspire wiring: OpenTelemetry, health checks, service discovery
|
|
- `src/Novelly.Mcp/` — MCP stdio server
|
|
- `src/Novelly.Web/` — React + Vite client
|
|
- `tests/` — test suite
|
|
- `docs/` — documentation
|
|
- `scripts/ci/` — bash CI steps; `prepush.sh` is what the Husky pre-push hook runs
|
|
|
|
## Best Practices
|
|
|
|
- Use latest .NET + latest supported nuget packages for that version
|
|
- Set `langVersion` to latest in all csproj files; enable nullable
|
|
- Organize code by feature/area, not layer or type. A new capability adds files to one feature folder
|
|
rather than a row to each of an entity/DTO/service/endpoint folder
|
|
- New features need unit tests covering logic as much as possible
|
|
- Modified file: check missing test coverage, all tests pass
|
|
|
|
## Architecture
|
|
|
|
- The React client, the embedded agent, and the MCP server all go through the same REST API. One source of truth — never let a client reach past the API to the database.
|
|
- Agent tools and MCP tools call the same application services the endpoints do. New capability = new service method, then surface it in all three.
|
|
- Domain has no dependencies. Application depends on Domain. Infrastructure depends on Application. Nothing depends on Api.
|
|
|
|
## Logging
|
|
|
|
Logging goes through Serilog, writing to the console, registered with `AddSerilog` rather than `UseSerilog` — this makes it an additional logging provider alongside the OpenTelemetry one that `AddServiceDefaults` wires up, instead of replacing it, so the Aspire dashboard keeps seeing structured logs. `app.UseSerilogRequestLogging()` must be registered before `app.UseExceptionHandler(...)` so it wraps the exception handler as the outermost middleware; otherwise it observes the raw exception as it propagates past it and logs the request as a 500 even when the exception handler goes on to turn it into a handled 404.
|
|
|
|
- Endpoints get their Information-level logging from a single `RequestLoggingEndpointFilter`, applied once per `MapGroup` call. No individual endpoint lambda needs to log anything itself.
|
|
- Every public service method logs an Information entry naming the operation and its identifying arguments — ids, enums, counts.
|
|
- Deeper, private helper methods log at Debug on entry and again before they return, including whatever state drove the decision.
|
|
- Anywhere an exception is caught, log it at Error with `LogError(ex, ...)`, including enough identifying values (ids, not prose) to debug from.
|
|
- Expected or recoverable failures — a not-found lookup, a validation rejection, an agent tool call that fails and is reported back to the model instead of thrown — log at Warning immediately before the throw or the recovery, not at Error.
|
|
- Always use structured message templates with named holes, like `{ChapterId}`, rather than string interpolation, so Serilog captures them as properties.
|
|
- Never log prose bodies (summary, notes, scene prose, agent message content) or the `ANTHROPIC_API_KEY`. Where a prose field is worth acknowledging in a log, log its length instead of its content.
|
|
|
|
# Coding
|
|
|
|
- Descriptive names all classes/methods. No generic: Provider, Manager, Helper
|
|
- Match formatting/style from `.editorconfig`
|
|
- Wrap lines at 220 chars, single line if fewer
|
|
- Interfaces implemented by single class → bottom of class file. Interface w/ multiple implementations → separate file.
|
|
- No tuples for return types. Prefer records or classes for multiple values
|
|
- Use `record` for data objects, `class` for objects with behavior. Avoid mutable state where possible.
|
|
- DTOs are records; entities are classes
|
|
- `PATCH` requests are partial: null field = leave alone, empty string = clear. Keep new update endpoints consistent with `Patch.Apply`.
|
|
- Enums cross the wire as names, never ordinals
|
|
|
|
## Testing
|
|
|
|
- Min 70% code coverage, target 90%. Unit tests focus end-user scenarios first.
|
|
- Don't write tests just for coverage. Call out missing coverage rather than cover stuff not valuable to end user.
|
|
- Code not cleanly unit-testable → mark `[ExcludeFromCodeCoverage]` or exclude namespace from coverage in .runsettings file
|
|
- BDD-style unit tests, end-to-end as possible. e.g. `Deleting_a_scene_leaves_its_beats_alone`
|
|
- NUnit. No FluentAssertions — assert with `Assert.That` and NUnit's constraint model:
|
|
`Assert.That(beat.SceneId, Is.Null)`, `Assert.That(listed, Has.Count.EqualTo(3))`,
|
|
`Assert.That(titles, Is.EqualTo(new[] { "First", "Second" }))`
|
|
- Grouping related asserts in `Assert.Multiple` beats a chain that stops at the first failure
|
|
- Expected exceptions: `Assert.That(() => service.Foo(), Throws.TypeOf<NotFoundException>())`
|
|
- Tests run against real in-memory SQLite via `TestDatabase`, not the EF InMemory provider — cascade deletes and query translation must be exercised, and the InMemory provider fakes both
|
|
- Model calls are faked at the `IAgentModelClient` seam (see `ScriptedModelClient`). Never hit the Anthropic API from a test.
|
|
- No "Mock" in mocked object names
|
|
- No Arrange/Act/Assert comments
|
|
- All tests pass before commit
|
|
|
|
## Verifying
|
|
|
|
Build and tests passing is not the same as working. For anything touching an endpoint, the agent loop, or the MCP server, run it:
|
|
|
|
- Everything at once: `dotnet run --project src/Novelly.AppHost` — Aspire starts the API on :5080 and
|
|
the Vite dev server on :5173, with the dashboard for logs and traces
|
|
- API alone: `ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api`, then exercise the route with curl
|
|
- Web alone: `cd src/Novelly.Web && npm run dev` — proxies `/api` to :5080
|
|
- MCP: build it, then drive it over stdio JSON-RPC (`initialize` → `notifications/initialized` → `tools/list` → `tools/call`)
|
|
|
|
Several real bugs here — SQLite refusing to ORDER BY a DateTimeOffset, the agent's model client throwing at construction and taking read-only endpoints down with it — passed the build and the test suite and only showed up when the app actually ran.
|
|
|
|
## Claude
|
|
|
|
- Plans = `.md` files in `docs/plans/`. Web → `docs/plans/web/`, API → `docs/plans/api/`
|
|
- Split large plans into discrete chunks — each buildable + committable independently
|
|
- Plan generated from `docs/plans/<name>.md` → save as `docs/plans/<name>_plan.md`
|
|
- Plan implemented from `docs/plans/<name>.md` → save summary as `docs/plans/<name>_output.md`
|
|
|
|
## Stack
|
|
|
|
`.gitignore` set for .NET/Visual Studio (C#, NuGet, MSBuild) plus Node. Update if stack change.
|
|
|
|
- Anthropic model id lives in `appsettings.json` under `Agent:Model`. Don't hardcode it.
|
|
- API key comes from `ANTHROPIC_API_KEY` or `Agent:ApiKey` — never commit one. The app must stay fully usable without a key; only the agent endpoints require it.
|
|
- EF migrations: `dotnet ef migrations add <Name> -p src/Novelly.Api -o Data/Migrations`. The API migrates on boot.
|
|
- `git push` runs `scripts/ci/prepush.sh` through Husky: build, test, then a web build. Run `npm install`
|
|
once at the repo root to install the hook.
|