Files
James Wampler e1df79b31e
CI / build-and-push (push) Successful in 47s
CI / deploy (push) Successful in 10s
Update docs and solution file for the stdio MCP server's removal
The prior commit's git add silently dropped these five files because
one path in the same invocation didn't exist. Docs, solution file, and
example config still needed the paired update: Novelly.slnx drops the
Novelly.Mcp project entry, README and CLAUDE.md describe MCP as an
in-API HTTP endpoint instead of a stdio binary, .mcp.json.example uses
the type: http shape, and outline-importer.md's tool references are
fixed to real tool names.
2026-08-21 11:00:25 -07:00

7.0 KiB

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, plus the MCP endpoint. One folder per feature holds entity, DTOs, service, endpoints together: Novels/, Characters/, Chapters/, Beats/, Scenes/, Tags/, Agent/, Mcp/. 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.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

  • No comments — no /// XML doc, no // line comments, no /* */ blocks, in C#, TS, or CSS. Unclear code → rename for clarity or extract a well-named method instead of explaining it.
  • 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
  • All frontend components should have an id attribute that identifies them uniquely.
  • Web client is keyboard-first: read docs/keyboard.md before adding any interactive UI (forms, editable rows, create flows).

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: with the API running, drive /mcp over HTTP (initializenotifications/initializedtools/listtools/call) — Streamable HTTP, so responses are SSE-framed and requests need Accept: application/json, text/event-stream

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.
  • Outline import root lives in appsettings.json under Imports:RootPath (Imports__RootPath env var). When set, it's the only folder the browse/upload import endpoints and the source picker can reach; unset, those endpoints are disabled and the dialog falls back to a typed path with no sandbox. Created at boot if missing.
  • git push runs scripts/ci/prepush.sh through Husky: build, test, then web build. Run npm install once at repo root to install hook.