Files
novelly/CLAUDE.original.md
James Wampler 04917fa09e Add Serilog console logging across the API
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.
2026-08-06 12:11:20 -07:00

7.5 KiB

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 (initializenotifications/initializedtools/listtools/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.