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.
105 lines
6.2 KiB
Markdown
105 lines
6.2 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for Claude Code (claude.ai/code) in repo.
|
|
|
|
## Project
|
|
|
|
Novelly: software plan + write novel. ASP.NET Core 10, C#, TypeScript, React, .NET Aspire. Chapter outlines, character dossiers, prose drafting, embedded Claude agent, MCP server over same API.
|
|
|
|
## Structure
|
|
|
|
- `src/Novelly.Api/` — whole back end, organised by feature. One folder per feature holds
|
|
entity, DTOs, service, endpoints together: `Projects/`, `Characters/`, `Chapters/`, `Beats/`,
|
|
`Scenes/`, `Tags/`, `Agent/`. `Common/` holds what crosses features; `Data/` holds
|
|
`DbContext` + EF migrations.
|
|
- `src/Novelly.AppHost/` — .NET Aspire orchestration; run this to bring up API + 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` = what Husky pre-push hook runs
|
|
|
|
## Best Practices
|
|
|
|
- Use latest .NET + latest supported nuget packages for that version
|
|
- Set `langVersion` latest in all csproj files; enable nullable
|
|
- Organize code by feature/area, not layer or type. New capability adds files to one feature folder
|
|
rather than row to each of entity/DTO/service/endpoint folder
|
|
- New features need unit tests covering logic much as possible
|
|
- Modified file: check missing test coverage, all tests pass
|
|
|
|
## Architecture
|
|
|
|
- React client, embedded agent, MCP server all go through same REST API. One source of truth — never let client reach past API to database.
|
|
- Agent tools + MCP tools call same application services endpoints do. New capability = new service method, then surface in all three.
|
|
|
|
## Logging
|
|
|
|
Serilog console via `AddSerilog` (not `UseSerilog` — keeps OTel provider for Aspire dashboard). `app.UseSerilogRequestLogging()` registered before `UseExceptionHandler` (outermost), else it logs raw exception status instead of handled one.
|
|
- Endpoints: Information via `RequestLoggingEndpointFilter` on each `MapGroup`. No per-lambda logging.
|
|
- Service public methods: Information at entry, ids/enums/counts as args.
|
|
- Deeper/private methods: Debug at start + end.
|
|
- Caught exceptions: `LogError(ex, ...)` with identifying values.
|
|
- Expected/recoverable (NotFound, validation, agent tool errors): Warning before throw/return.
|
|
- Structured templates only — `{ChapterId}`, never interpolation.
|
|
- Never log prose (summary/notes/content) or `ANTHROPIC_API_KEY`. Prose = length only.
|
|
|
|
# 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
|
|
- Interface 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. DO NOT use Dto in names.
|
|
- `PATCH` requests partial: null field = leave alone, empty string = clear. Keep new update endpoints consistent with `Patch.Apply`.
|
|
- Enums cross 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` + 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 chain that stops at first failure
|
|
- Expected exceptions: `Assert.That(() => service.Foo(), Throws.TypeOf<NotFoundException>())`
|
|
- Tests run against real in-memory SQLite via `TestDatabase`, not EF InMemory provider — cascade deletes + query translation must be exercised, InMemory provider fakes both
|
|
- Model calls faked at `IAgentModelClient` seam (see `ScriptedModelClient`). Never hit Anthropic API from test.
|
|
- No "Mock" in mocked object names
|
|
- No Arrange/Act/Assert comments
|
|
- All tests pass before commit
|
|
|
|
## Verifying
|
|
|
|
Build + tests passing ≠ working. Anything touching endpoint, agent loop, or MCP server — run it:
|
|
|
|
- Everything at once: `dotnet run --project src/Novelly.AppHost` — Aspire starts API on :5080 +
|
|
Vite dev server on :5173, dashboard for logs + traces
|
|
- API alone: `ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api`, then exercise route with curl
|
|
- Web alone: `cd src/Novelly.Web && npm run dev` — proxies `/api` to :5080
|
|
- MCP: build it, then drive over stdio JSON-RPC (`initialize` → `notifications/initialized` → `tools/list` → `tools/call`)
|
|
|
|
Several real bugs here — SQLite refusing ORDER BY DateTimeOffset, agent's model client throwing at construction + taking read-only endpoints down with it — passed build + test suite, only showed up when 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.
|
|
- API key comes from `ANTHROPIC_API_KEY` or `Agent:ApiKey` — never commit one. App must stay fully usable without key; only agent endpoints require it.
|
|
- EF migrations: `dotnet ef migrations add <Name> -p src/Novelly.Api -o Data/Migrations`. API migrates on boot.
|
|
- `git push` runs `scripts/ci/prepush.sh` through Husky: build, test, then web build. Run `npm install`
|
|
once at repo root to install hook.
|