From 725758ccd9c5362c5528c5fbf39f6b0a277eb1b8 Mon Sep 17 00:00:00 2001 From: James Wampler Date: Thu, 6 Aug 2026 05:05:06 +0000 Subject: [PATCH] Reorganise by feature, rename to Novelly, add Aspire and a pre-push hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The layered split into Domain/Application/Infrastructure/Api was forcing organisation by layer: adding one capability meant touching four projects and four folders that each held a slice of it. Those four projects are now one feature-organised Novelly.Api, where each folder — Projects, Characters, Chapters, Beats, Scenes, Tags, Agent — holds its entity, DTOs, service and endpoints together. Common/ holds what genuinely crosses features (the patch semantics, the two exception types, DraftStatus) and Data/ holds the DbContext and migrations. Six .NET projects become five: the three layer projects are gone, and Novelly.AppHost and Novelly.ServiceDefaults are new. - Namespaces move from NovelSoftware.* to Novelly.*, including the entity type names recorded in the EF model snapshots. The migration ids are untouched, so an existing novel.db still migrates cleanly — verified against a fresh file. - Aspire orchestration mirrors the mic-check setup: the AppHost starts the API on :5080 and the Vite dev server on :5173, and the API picks up OpenTelemetry, health checks and service discovery from ServiceDefaults. /health and /alive now answer in development. - A Husky pre-push hook runs scripts/ci/prepush.sh: build, test, then a web build. The scripts are plain bash so CI can run the same steps. - The MCP server's env var is now NOVELLY_API_URL. Verified beyond the build: 44 tests pass, the web client builds, the API was exercised over curl (project/chapter/beat/tag round trip, tag cross-reference, 503 on the agent without a key while conversation listing still returns 200), the MCP server was driven over stdio JSON-RPC (26 tools, errors still surface the API's own message rather than being flattened), and the AppHost was run to confirm both resources come up and Vite proxies /api through to the API. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw --- .gitignore | 6 +- .husky/pre-push | 1 + .mcp.json.example | 6 +- CLAUDE.md | 30 +++-- NovelSoftware.slnx | 12 -- Novelly.slnx | 17 +++ README.md | 52 ++++--- package-lock.json | 29 ++++ package.json | 11 ++ scripts/ci/lib.sh | 38 ++++++ scripts/ci/prepush.sh | 23 ++++ .../NovelSoftware.Api.csproj | 23 ---- .../NovelSoftware.Application.csproj | 21 --- src/NovelSoftware.Domain/Enums.cs | 31 ----- .../NovelSoftware.Domain.csproj | 10 -- .../Agent/AgentContracts.cs | 2 +- .../Agent}/AgentConversation.cs | 4 +- .../Dtos => Novelly.Api/Agent}/AgentDtos.cs | 4 +- .../Agent}/AgentEndpoints.cs | 5 +- src/Novelly.Api/Agent/AgentRole.cs | 8 ++ .../Agent}/AnthropicAgentModelClient.cs | 7 +- .../Agent/JsonSchema.cs | 4 +- .../Agent/NovelAgentService.cs | 10 +- .../Agent/NovelAgentToolset.cs | 12 +- .../Entities => Novelly.Api/Beats}/Beat.cs | 7 +- .../Dtos => Novelly.Api/Beats}/BeatDtos.cs | 4 +- .../Beats}/BeatEndpoints.cs | 5 +- .../Beats}/BeatService.cs | 8 +- .../Chapters}/Chapter.cs | 9 +- .../Chapters}/ChapterDtos.cs | 8 +- .../Chapters}/ChapterEndpoints.cs | 5 +- .../Chapters}/ChapterService.cs | 8 +- .../Characters}/Character.cs | 5 +- .../Characters}/CharacterDtos.cs | 5 +- .../Characters}/CharacterEndpoints.cs | 5 +- src/Novelly.Api/Characters/CharacterRole.cs | 14 ++ .../Characters}/CharacterService.cs | 8 +- .../Common}/AgentNotConfiguredException.cs | 2 +- src/Novelly.Api/Common/DraftStatus.cs | 11 ++ .../Common}/NotFoundException.cs | 2 +- .../Common/NovellyServiceRegistration.cs} | 26 ++-- src/Novelly.Api/Common/Patch.cs | 15 +++ .../Data}/INovelDbContext.cs | 10 +- .../20260806023249_InitialSchema.Designer.cs | 68 +++++----- .../20260806023249_InitialSchema.cs | 2 +- ...ReplaceOutlineWithBeatsAndTags.Designer.cs | 84 ++++++------ ...06031243_ReplaceOutlineWithBeatsAndTags.cs | 2 +- .../Migrations/NovelDbContextModelSnapshot.cs | 84 ++++++------ .../Data}/NovelDbContext.cs | 13 +- .../Novelly.Api.csproj} | 9 +- .../Program.cs | 18 ++- .../Projects}/Project.cs | 7 +- .../Projects}/ProjectDtos.cs | 4 +- .../Projects}/ProjectEndpoints.cs | 5 +- .../Projects}/ProjectService.cs | 20 +-- .../Properties/launchSettings.json | 0 .../Entities => Novelly.Api/Scenes}/Scene.cs | 6 +- .../Dtos => Novelly.Api/Scenes}/SceneDtos.cs | 5 +- .../Scenes}/SceneEndpoints.cs | 5 +- .../Scenes}/SceneService.cs | 7 +- .../Entities => Novelly.Api/Tags}/Tag.cs | 7 +- .../Dtos => Novelly.Api/Tags}/TagDtos.cs | 4 +- .../Tags}/TagEndpoints.cs | 5 +- .../Tags}/TagService.cs | 7 +- .../appsettings.Development.json | 2 +- .../appsettings.json | 0 src/Novelly.AppHost/AppHost.cs | 15 +++ src/Novelly.AppHost/Novelly.AppHost.csproj | 21 +++ .../Properties/launchSettings.json | 29 ++++ .../appsettings.Development.json | 16 +++ src/Novelly.AppHost/appsettings.json | 9 ++ src/Novelly.AppHost/aspire.config.json | 5 + .../NovelApiClient.cs | 6 +- .../Novelly.Mcp.csproj} | 0 .../Program.cs | 4 +- .../Tools/BeatTools.cs | 2 +- .../Tools/CharacterTools.cs | 2 +- .../Tools/ManuscriptTools.cs | 2 +- .../Tools/ProjectTools.cs | 2 +- .../Tools/TagTools.cs | 2 +- src/Novelly.ServiceDefaults/Extensions.cs | 127 ++++++++++++++++++ .../Novelly.ServiceDefaults.csproj | 24 ++++ .../.gitignore | 0 .../.oxlintrc.json | 0 .../README.md | 0 .../index.html | 0 .../package-lock.json | 4 +- .../package.json | 2 +- .../public/favicon.svg | 0 .../public/icons.svg | 0 .../src/App.tsx | 0 .../src/api/client.ts | 0 .../src/api/hooks.ts | 0 .../src/api/types.ts | 2 +- .../src/components/TagEditor.tsx | 0 .../src/components/ui.tsx | 0 .../src/index.css | 0 .../src/main.tsx | 0 .../src/pages/AgentPage.tsx | 0 .../src/pages/ChapterPage.tsx | 0 .../src/pages/ChaptersPage.tsx | 0 .../src/pages/CharactersPage.tsx | 0 .../src/pages/OverviewPage.tsx | 0 .../src/pages/ProjectLayout.tsx | 0 .../src/pages/ProjectsPage.tsx | 0 .../src/pages/TagsPage.tsx | 0 .../src/vite-env.d.ts | 0 .../tsconfig.app.json | 0 .../tsconfig.json | 0 .../tsconfig.node.json | 0 .../vite.config.ts | 0 .../AnthropicClientTests.cs | 7 +- .../BeatServiceTests.cs | 10 +- .../ListingTests.cs | 10 +- .../NovelAgentServiceTests.cs | 6 +- .../Novelly.Api.Tests.csproj} | 7 +- .../ProjectDataTests.cs | 10 +- .../ServiceTestFixture.cs | 9 +- .../TagServiceTests.cs | 8 +- .../TestDatabase.cs | 4 +- 120 files changed, 811 insertions(+), 421 deletions(-) create mode 100755 .husky/pre-push delete mode 100644 NovelSoftware.slnx create mode 100644 Novelly.slnx create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 scripts/ci/lib.sh create mode 100755 scripts/ci/prepush.sh delete mode 100644 src/NovelSoftware.Api/NovelSoftware.Api.csproj delete mode 100644 src/NovelSoftware.Application/NovelSoftware.Application.csproj delete mode 100644 src/NovelSoftware.Domain/Enums.cs delete mode 100644 src/NovelSoftware.Domain/NovelSoftware.Domain.csproj rename src/{NovelSoftware.Application => Novelly.Api}/Agent/AgentContracts.cs (98%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Agent}/AgentConversation.cs (96%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Agent}/AgentDtos.cs (91%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Agent}/AgentEndpoints.cs (93%) create mode 100644 src/Novelly.Api/Agent/AgentRole.cs rename src/{NovelSoftware.Infrastructure/Anthropic => Novelly.Api/Agent}/AnthropicAgentModelClient.cs (97%) rename src/{NovelSoftware.Application => Novelly.Api}/Agent/JsonSchema.cs (99%) rename src/{NovelSoftware.Application => Novelly.Api}/Agent/NovelAgentService.cs (98%) rename src/{NovelSoftware.Application => Novelly.Api}/Agent/NovelAgentToolset.cs (98%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Beats}/Beat.cs (91%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Beats}/BeatDtos.cs (95%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Beats}/BeatEndpoints.cs (94%) rename src/{NovelSoftware.Application/Services => Novelly.Api/Beats}/BeatService.cs (97%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Chapters}/Chapter.cs (87%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Chapters}/ChapterDtos.cs (95%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Chapters}/ChapterEndpoints.cs (93%) rename src/{NovelSoftware.Application/Services => Novelly.Api/Chapters}/ChapterService.cs (96%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Characters}/Character.cs (96%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Characters}/CharacterDtos.cs (96%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Characters}/CharacterEndpoints.cs (95%) create mode 100644 src/Novelly.Api/Characters/CharacterRole.cs rename src/{NovelSoftware.Application/Services => Novelly.Api/Characters}/CharacterService.cs (97%) rename src/{NovelSoftware.Application => Novelly.Api/Common}/AgentNotConfiguredException.cs (89%) create mode 100644 src/Novelly.Api/Common/DraftStatus.cs rename src/{NovelSoftware.Application => Novelly.Api/Common}/NotFoundException.cs (90%) rename src/{NovelSoftware.Infrastructure/DependencyInjection.cs => Novelly.Api/Common/NovellyServiceRegistration.cs} (59%) create mode 100644 src/Novelly.Api/Common/Patch.cs rename src/{NovelSoftware.Application => Novelly.Api/Data}/INovelDbContext.cs (77%) rename src/{NovelSoftware.Infrastructure/Persistence => Novelly.Api/Data}/Migrations/20260806023249_InitialSchema.Designer.cs (84%) rename src/{NovelSoftware.Infrastructure/Persistence => Novelly.Api/Data}/Migrations/20260806023249_InitialSchema.cs (99%) rename src/{NovelSoftware.Infrastructure/Persistence => Novelly.Api/Data}/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs (85%) rename src/{NovelSoftware.Infrastructure/Persistence => Novelly.Api/Data}/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs (99%) rename src/{NovelSoftware.Infrastructure/Persistence => Novelly.Api/Data}/Migrations/NovelDbContextModelSnapshot.cs (85%) rename src/{NovelSoftware.Infrastructure/Persistence => Novelly.Api/Data}/NovelDbContext.cs (96%) rename src/{NovelSoftware.Infrastructure/NovelSoftware.Infrastructure.csproj => Novelly.Api/Novelly.Api.csproj} (72%) rename src/{NovelSoftware.Api => Novelly.Api}/Program.cs (87%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Projects}/Project.cs (88%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Projects}/ProjectDtos.cs (94%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Projects}/ProjectEndpoints.cs (92%) rename src/{NovelSoftware.Application/Services => Novelly.Api/Projects}/ProjectService.cs (84%) rename src/{NovelSoftware.Api => Novelly.Api}/Properties/launchSettings.json (100%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Scenes}/Scene.cs (91%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Scenes}/SceneDtos.cs (93%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Scenes}/SceneEndpoints.cs (93%) rename src/{NovelSoftware.Application/Services => Novelly.Api/Scenes}/SceneService.cs (96%) rename src/{NovelSoftware.Domain/Entities => Novelly.Api/Tags}/Tag.cs (85%) rename src/{NovelSoftware.Application/Dtos => Novelly.Api/Tags}/TagDtos.cs (95%) rename src/{NovelSoftware.Api/Endpoints => Novelly.Api/Tags}/TagEndpoints.cs (93%) rename src/{NovelSoftware.Application/Services => Novelly.Api/Tags}/TagService.cs (97%) rename src/{NovelSoftware.Api => Novelly.Api}/appsettings.Development.json (78%) rename src/{NovelSoftware.Api => Novelly.Api}/appsettings.json (100%) create mode 100644 src/Novelly.AppHost/AppHost.cs create mode 100644 src/Novelly.AppHost/Novelly.AppHost.csproj create mode 100755 src/Novelly.AppHost/Properties/launchSettings.json create mode 100755 src/Novelly.AppHost/appsettings.Development.json create mode 100755 src/Novelly.AppHost/appsettings.json create mode 100755 src/Novelly.AppHost/aspire.config.json rename src/{NovelSoftware.Mcp => Novelly.Mcp}/NovelApiClient.cs (93%) rename src/{NovelSoftware.Mcp/NovelSoftware.Mcp.csproj => Novelly.Mcp/Novelly.Mcp.csproj} (100%) rename src/{NovelSoftware.Mcp => Novelly.Mcp}/Program.cs (87%) rename src/{NovelSoftware.Mcp => Novelly.Mcp}/Tools/BeatTools.cs (99%) rename src/{NovelSoftware.Mcp => Novelly.Mcp}/Tools/CharacterTools.cs (99%) rename src/{NovelSoftware.Mcp => Novelly.Mcp}/Tools/ManuscriptTools.cs (99%) rename src/{NovelSoftware.Mcp => Novelly.Mcp}/Tools/ProjectTools.cs (98%) rename src/{NovelSoftware.Mcp => Novelly.Mcp}/Tools/TagTools.cs (98%) create mode 100755 src/Novelly.ServiceDefaults/Extensions.cs create mode 100644 src/Novelly.ServiceDefaults/Novelly.ServiceDefaults.csproj rename src/{NovelSoftware.Web => Novelly.Web}/.gitignore (100%) rename src/{NovelSoftware.Web => Novelly.Web}/.oxlintrc.json (100%) rename src/{NovelSoftware.Web => Novelly.Web}/README.md (100%) rename src/{NovelSoftware.Web => Novelly.Web}/index.html (100%) rename src/{NovelSoftware.Web => Novelly.Web}/package-lock.json (99%) rename src/{NovelSoftware.Web => Novelly.Web}/package.json (95%) rename src/{NovelSoftware.Web => Novelly.Web}/public/favicon.svg (100%) rename src/{NovelSoftware.Web => Novelly.Web}/public/icons.svg (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/App.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/api/client.ts (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/api/hooks.ts (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/api/types.ts (97%) rename src/{NovelSoftware.Web => Novelly.Web}/src/components/TagEditor.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/components/ui.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/index.css (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/main.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/AgentPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/ChapterPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/ChaptersPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/CharactersPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/OverviewPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/ProjectLayout.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/ProjectsPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/pages/TagsPage.tsx (100%) rename src/{NovelSoftware.Web => Novelly.Web}/src/vite-env.d.ts (100%) rename src/{NovelSoftware.Web => Novelly.Web}/tsconfig.app.json (100%) rename src/{NovelSoftware.Web => Novelly.Web}/tsconfig.json (100%) rename src/{NovelSoftware.Web => Novelly.Web}/tsconfig.node.json (100%) rename src/{NovelSoftware.Web => Novelly.Web}/vite.config.ts (100%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/AnthropicClientTests.cs (89%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/BeatServiceTests.cs (97%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/ListingTests.cs (97%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/NovelAgentServiceTests.cs (98%) rename tests/{NovelSoftware.Tests/NovelSoftware.Tests.csproj => Novelly.Api.Tests/Novelly.Api.Tests.csproj} (78%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/ProjectDataTests.cs (97%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/ServiceTestFixture.cs (88%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/TagServiceTests.cs (97%) rename tests/{NovelSoftware.Tests => Novelly.Api.Tests}/TestDatabase.cs (94%) diff --git a/.gitignore b/.gitignore index aefa0f0..4f43f5b 100644 --- a/.gitignore +++ b/.gitignore @@ -428,10 +428,14 @@ FodyWeavers.xsd *.msm *.msp -## Novel Software +## Novelly node_modules/ dist/ *.db *.db-shm *.db-wal mcp-server/ + +# Toolchains installed locally by scripts/ci/lib.sh +.dotnet/ +.node/ diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..d8d14fe --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +exec ./scripts/ci/prepush.sh diff --git a/.mcp.json.example b/.mcp.json.example index e526305..71f03a2 100644 --- a/.mcp.json.example +++ b/.mcp.json.example @@ -1,9 +1,9 @@ { "mcpServers": { - "novel-software": { - "command": "./mcp-server/NovelSoftware.Mcp", + "novelly": { + "command": "./mcp-server/Novelly.Mcp", "env": { - "NOVELSOFTWARE_API_URL": "http://localhost:5080" + "NOVELLY_API_URL": "http://localhost:5080" } } } diff --git a/CLAUDE.md b/CLAUDE.md index 1af19a1..a2fd8f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,24 +4,28 @@ Guidance for Claude Code (claude.ai/code) in this repo. ## Project -Novel Software: software for planning and writing a novel. ASP.NET Core 10, C#, TypeScript, React. Chapter outlines, character dossiers, prose drafting, an embedded Claude agent, and an MCP server over the same API. +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/NovelSoftware.Domain/` — entities and enums, no dependencies -- `src/NovelSoftware.Application/` — services, DTOs, the agent tool-use loop -- `src/NovelSoftware.Infrastructure/` — EF Core + SQLite, Anthropic SDK client -- `src/NovelSoftware.Api/` — minimal API endpoints -- `src/NovelSoftware.Mcp/` — MCP stdio server -- `src/NovelSoftware.Web/` — React + Vite client +- `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 type +- 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 @@ -64,8 +68,10 @@ Novel Software: software for planning and writing a novel. ASP.NET Core 10, C#, Build and tests passing is not the same as working. For anything touching an endpoint, the agent loop, or the MCP server, run it: -- API: `ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/NovelSoftware.Api`, then exercise the route with curl -- Web: `cd src/NovelSoftware.Web && npm run dev` — proxies `/api` to :5080 +- 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. @@ -83,4 +89,6 @@ Several real bugs here — SQLite refusing to ORDER BY a DateTimeOffset, the age - 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 -p src/NovelSoftware.Infrastructure -s src/NovelSoftware.Api -o Persistence/Migrations`. The API migrates on boot. +- EF migrations: `dotnet ef migrations add -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. diff --git a/NovelSoftware.slnx b/NovelSoftware.slnx deleted file mode 100644 index d2c7a3f..0000000 --- a/NovelSoftware.slnx +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Novelly.slnx b/Novelly.slnx new file mode 100644 index 0000000..ff9b8b5 --- /dev/null +++ b/Novelly.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 99d1b5a..f2b4788 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Novel Software +# Novelly Software for planning and writing a novel. You outline the book, keep character dossiers, break chapters into scenes, and draft prose — with a Claude-powered agent @@ -14,23 +14,40 @@ same edit. | Piece | Built with | |---|---| -| `NovelSoftware.Api` | ASP.NET Core 10 minimal APIs, OpenAPI | -| `NovelSoftware.Application` | Services, DTOs, the agent tool-use loop | -| `NovelSoftware.Domain` | Entities and enums, no dependencies | -| `NovelSoftware.Infrastructure` | EF Core 10 + SQLite, Anthropic SDK client | -| `NovelSoftware.Mcp` | MCP stdio server (`ModelContextProtocol`) | -| `NovelSoftware.Web` | React 19, TypeScript, Vite, TanStack Query, Tailwind v4 | +| `Novelly.Api` | ASP.NET Core 10 minimal APIs, EF Core 10 + SQLite, Anthropic SDK, OpenAPI | +| `Novelly.AppHost` | .NET Aspire orchestration for the API and the web client | +| `Novelly.ServiceDefaults` | Shared OpenTelemetry, health checks and service discovery | +| `Novelly.Mcp` | MCP stdio server (`ModelContextProtocol`) | +| `Novelly.Web` | React 19, TypeScript, Vite, TanStack Query, Tailwind v4 | + +The back end is one project organised by feature, not by layer. Each feature folder — +`Projects/`, `Characters/`, `Chapters/`, `Beats/`, `Scenes/`, `Tags/`, `Agent/` — holds its +entity, DTOs, service and endpoints together, so adding a capability means touching one +folder rather than four. `Common/` holds what genuinely crosses features and `Data/` holds +the `DbContext` and migrations. ## Running it Prerequisites: .NET 10 SDK and Node 20+. +Everything at once, through Aspire: + +```bash +cd src/Novelly.Web && npm install && cd - +dotnet run --project src/Novelly.AppHost +``` + +That starts the API on :5080 and the Vite dev server on :5173, and opens the Aspire +dashboard for logs, traces and metrics across both. + +Or run the two halves separately: + ```bash # 1. API — creates and migrates novel.db on first run, listens on :5080 -ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/NovelSoftware.Api +ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api # 2. Web — dev server on :5173, proxies /api to :5080 -cd src/NovelSoftware.Web && npm install && npm run dev +cd src/Novelly.Web && npm install && npm run dev ``` Open http://localhost:5173. @@ -48,16 +65,19 @@ and everything else keeps working. ### Tests ```bash -dotnet test # 44 tests -cd src/NovelSoftware.Web && npm run build # typecheck + bundle +dotnet test # 44 tests +cd src/Novelly.Web && npm run build # typecheck + bundle ``` +`git push` runs both through a Husky pre-push hook (`scripts/ci/prepush.sh`). Install the +hook once with `npm install` at the repo root. + Tests run against real in-memory SQLite rather than the EF in-memory provider, so they exercise the cascade deletes and query translation the app actually ships with. ## Configuration -`src/NovelSoftware.Api/appsettings.json`: +`src/Novelly.Api/appsettings.json`: ```jsonc { @@ -139,7 +159,7 @@ its own — it is a second front end, not a second implementation. Build it, then point your MCP client at the produced binary: ```bash -dotnet publish src/NovelSoftware.Mcp -c Release -o ./mcp-server +dotnet publish src/Novelly.Mcp -c Release -o ./mcp-server ``` `.mcp.json` (or Claude Desktop's config): @@ -147,9 +167,9 @@ dotnet publish src/NovelSoftware.Mcp -c Release -o ./mcp-server ```jsonc { "mcpServers": { - "novel-software": { - "command": "/absolute/path/to/mcp-server/NovelSoftware.Mcp", - "env": { "NOVELSOFTWARE_API_URL": "http://localhost:5080" } + "novelly": { + "command": "/absolute/path/to/mcp-server/Novelly.Mcp", + "env": { "NOVELLY_API_URL": "http://localhost:5080" } } } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..247db1e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "novelly", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "novelly", + "devDependencies": { + "husky": "^9.1.7" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b097bc --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "novelly", + "private": true, + "description": "Repo-root package - hosts the Husky pre-push hook only.", + "scripts": { + "prepare": "husky" + }, + "devDependencies": { + "husky": "^9.1.7" + } +} diff --git a/scripts/ci/lib.sh b/scripts/ci/lib.sh new file mode 100755 index 0000000..3136341 --- /dev/null +++ b/scripts/ci/lib.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Shared helpers for scripts/ci/*.sh. Everything here is plain bash so the same script +# runs identically on a developer's machine and on a bare CI runner. +set -euo pipefail + +log() { + echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*" +} + +fail() { + echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] ERROR: $*" >&2 + exit 1 +} + +# Repo root, regardless of the caller's cwd. +CI_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# Prepends a locally installed .NET SDK to PATH when `dotnet` is missing, so a runner +# with no SDK preinstalled behaves the same as a dev machine. +ensure_dotnet() { + if command -v dotnet > /dev/null 2>&1; then + return 0 + fi + + local install_dir="$CI_ROOT/.dotnet" + if [[ ! -x "$install_dir/dotnet" ]]; then + log "dotnet not found on PATH; installing the .NET SDK" + curl -fsSL https://dot.net/v1/dotnet-install.sh -o "/tmp/dotnet-install.sh" + bash "/tmp/dotnet-install.sh" --channel STS --install-dir "$install_dir" + rm -f "/tmp/dotnet-install.sh" + fi + export PATH="$install_dir:$PATH" + export DOTNET_ROOT="$install_dir" +} + +ensure_node() { + command -v npm > /dev/null 2>&1 || fail "npm not found on PATH; install Node.js to build the web client" +} diff --git a/scripts/ci/prepush.sh b/scripts/ci/prepush.sh new file mode 100755 index 0000000..c3e42ec --- /dev/null +++ b/scripts/ci/prepush.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Husky pre-push hook body. Compiles the solution, runs the test suite and builds the +# web client, so broken code never leaves the workstation. +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" && source ./lib.sh +cd "$CI_ROOT" + +ensure_dotnet +ensure_node + +log "Building the solution (Debug)" +dotnet build Novelly.slnx -c Debug + +log "Running Novelly.Api.Tests" +dotnet test tests/Novelly.Api.Tests/Novelly.Api.Tests.csproj -c Debug + +log "Installing web dependencies (npm ci)" +npm --prefix src/Novelly.Web ci + +log "Building the web client (vite build)" +npm --prefix src/Novelly.Web run build + +log "prepush.sh complete - ok to push" diff --git a/src/NovelSoftware.Api/NovelSoftware.Api.csproj b/src/NovelSoftware.Api/NovelSoftware.Api.csproj deleted file mode 100644 index 39aff4a..0000000 --- a/src/NovelSoftware.Api/NovelSoftware.Api.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - net10.0 - enable - latest - enable - - - diff --git a/src/NovelSoftware.Application/NovelSoftware.Application.csproj b/src/NovelSoftware.Application/NovelSoftware.Application.csproj deleted file mode 100644 index 6e8c81d..0000000 --- a/src/NovelSoftware.Application/NovelSoftware.Application.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - net10.0 - enable - enable - latest - - - diff --git a/src/NovelSoftware.Domain/Enums.cs b/src/NovelSoftware.Domain/Enums.cs deleted file mode 100644 index f6f8f80..0000000 --- a/src/NovelSoftware.Domain/Enums.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace NovelSoftware.Domain; - -/// The role a character plays in the story. -public enum CharacterRole -{ - Protagonist, - Antagonist, - Deuteragonist, - Supporting, - Minor, - Mentor, - LoveInterest, - Foil -} - -/// How far along a chapter or scene is in the drafting pipeline. -public enum DraftStatus -{ - Planned, - Outlined, - Drafted, - Revised, - Final -} - -/// Who produced a message in an agent conversation. -public enum AgentRole -{ - User, - Assistant -} diff --git a/src/NovelSoftware.Domain/NovelSoftware.Domain.csproj b/src/NovelSoftware.Domain/NovelSoftware.Domain.csproj deleted file mode 100644 index 5bdc6ca..0000000 --- a/src/NovelSoftware.Domain/NovelSoftware.Domain.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - net10.0 - enable - enable - latest - - - diff --git a/src/NovelSoftware.Application/Agent/AgentContracts.cs b/src/Novelly.Api/Agent/AgentContracts.cs similarity index 98% rename from src/NovelSoftware.Application/Agent/AgentContracts.cs rename to src/Novelly.Api/Agent/AgentContracts.cs index 6018d38..ee5d04a 100644 --- a/src/NovelSoftware.Application/Agent/AgentContracts.cs +++ b/src/Novelly.Api/Agent/AgentContracts.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace NovelSoftware.Application.Agent; +namespace Novelly.Api.Agent; /// A tool the model may call, described in the shape the Messages API expects. public record AgentToolDefinition(string Name, string Description, JsonElement InputSchema); diff --git a/src/NovelSoftware.Domain/Entities/AgentConversation.cs b/src/Novelly.Api/Agent/AgentConversation.cs similarity index 96% rename from src/NovelSoftware.Domain/Entities/AgentConversation.cs rename to src/Novelly.Api/Agent/AgentConversation.cs index d4c7070..20596cd 100644 --- a/src/NovelSoftware.Domain/Entities/AgentConversation.cs +++ b/src/Novelly.Api/Agent/AgentConversation.cs @@ -1,4 +1,6 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Projects; + +namespace Novelly.Api.Agent; /// A chat thread between the writer and the embedded agent, scoped to one project. public class AgentConversation diff --git a/src/NovelSoftware.Application/Dtos/AgentDtos.cs b/src/Novelly.Api/Agent/AgentDtos.cs similarity index 91% rename from src/NovelSoftware.Application/Dtos/AgentDtos.cs rename to src/Novelly.Api/Agent/AgentDtos.cs index 4e771b1..d60eb70 100644 --- a/src/NovelSoftware.Application/Dtos/AgentDtos.cs +++ b/src/Novelly.Api/Agent/AgentDtos.cs @@ -1,6 +1,4 @@ -using NovelSoftware.Domain; - -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Agent; public record ConversationSummaryDto( Guid Id, diff --git a/src/NovelSoftware.Api/Endpoints/AgentEndpoints.cs b/src/Novelly.Api/Agent/AgentEndpoints.cs similarity index 93% rename from src/NovelSoftware.Api/Endpoints/AgentEndpoints.cs rename to src/Novelly.Api/Agent/AgentEndpoints.cs index f268191..a8ae893 100644 --- a/src/NovelSoftware.Api/Endpoints/AgentEndpoints.cs +++ b/src/Novelly.Api/Agent/AgentEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Agent; -using NovelSoftware.Application.Dtos; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Agent; public static class AgentEndpoints { diff --git a/src/Novelly.Api/Agent/AgentRole.cs b/src/Novelly.Api/Agent/AgentRole.cs new file mode 100644 index 0000000..4c46591 --- /dev/null +++ b/src/Novelly.Api/Agent/AgentRole.cs @@ -0,0 +1,8 @@ +namespace Novelly.Api.Agent; + +/// Who produced a message in an agent conversation. +public enum AgentRole +{ + User, + Assistant +} diff --git a/src/NovelSoftware.Infrastructure/Anthropic/AnthropicAgentModelClient.cs b/src/Novelly.Api/Agent/AnthropicAgentModelClient.cs similarity index 97% rename from src/NovelSoftware.Infrastructure/Anthropic/AnthropicAgentModelClient.cs rename to src/Novelly.Api/Agent/AnthropicAgentModelClient.cs index f106c8a..6ee52b5 100644 --- a/src/NovelSoftware.Infrastructure/Anthropic/AnthropicAgentModelClient.cs +++ b/src/Novelly.Api/Agent/AnthropicAgentModelClient.cs @@ -1,11 +1,10 @@ using System.Text.Json; -using Anthropic; using Anthropic.Models.Messages; +using Anthropic; using Microsoft.Extensions.Options; -using NovelSoftware.Application; -using NovelSoftware.Application.Agent; +using Novelly.Api.Common; -namespace NovelSoftware.Infrastructure.Anthropic; +namespace Novelly.Api.Agent; /// /// Talks to the Anthropic Messages API. Translates between the application's diff --git a/src/NovelSoftware.Application/Agent/JsonSchema.cs b/src/Novelly.Api/Agent/JsonSchema.cs similarity index 99% rename from src/NovelSoftware.Application/Agent/JsonSchema.cs rename to src/Novelly.Api/Agent/JsonSchema.cs index 67020aa..a2e5b86 100644 --- a/src/NovelSoftware.Application/Agent/JsonSchema.cs +++ b/src/Novelly.Api/Agent/JsonSchema.cs @@ -1,7 +1,7 @@ -using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json; -namespace NovelSoftware.Application.Agent; +namespace Novelly.Api.Agent; /// /// Small builder for the JSON Schema objects tool definitions need. Hand-writing these diff --git a/src/NovelSoftware.Application/Agent/NovelAgentService.cs b/src/Novelly.Api/Agent/NovelAgentService.cs similarity index 98% rename from src/NovelSoftware.Application/Agent/NovelAgentService.cs rename to src/Novelly.Api/Agent/NovelAgentService.cs index 986f6e9..5c1bbca 100644 --- a/src/NovelSoftware.Application/Agent/NovelAgentService.cs +++ b/src/Novelly.Api/Agent/NovelAgentService.cs @@ -1,13 +1,13 @@ -using System.Text; using System.Text.Json; +using System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Common; +using Novelly.Api.Data; +using Novelly.Api.Projects; -namespace NovelSoftware.Application.Agent; +namespace Novelly.Api.Agent; /// /// The embedded writing agent. Runs the tool-use loop against the model, persists the diff --git a/src/NovelSoftware.Application/Agent/NovelAgentToolset.cs b/src/Novelly.Api/Agent/NovelAgentToolset.cs similarity index 98% rename from src/NovelSoftware.Application/Agent/NovelAgentToolset.cs rename to src/Novelly.Api/Agent/NovelAgentToolset.cs index 3ee2f38..a5a3afa 100644 --- a/src/NovelSoftware.Application/Agent/NovelAgentToolset.cs +++ b/src/Novelly.Api/Agent/NovelAgentToolset.cs @@ -1,9 +1,13 @@ using System.Text.Json; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; -using NovelSoftware.Domain; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Common; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Agent; +namespace Novelly.Api.Agent; /// The outcome of running a tool: what to hand back to the model, and whether it failed. public record AgentToolResult(string Content, bool IsError); diff --git a/src/NovelSoftware.Domain/Entities/Beat.cs b/src/Novelly.Api/Beats/Beat.cs similarity index 91% rename from src/NovelSoftware.Domain/Entities/Beat.cs rename to src/Novelly.Api/Beats/Beat.cs index 3baae4c..0d014d5 100644 --- a/src/NovelSoftware.Domain/Entities/Beat.cs +++ b/src/Novelly.Api/Beats/Beat.cs @@ -1,4 +1,9 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; + +namespace Novelly.Api.Beats; /// /// One row of a chapter's outline: a short label, who it belongs to, what happened, and diff --git a/src/NovelSoftware.Application/Dtos/BeatDtos.cs b/src/Novelly.Api/Beats/BeatDtos.cs similarity index 95% rename from src/NovelSoftware.Application/Dtos/BeatDtos.cs rename to src/Novelly.Api/Beats/BeatDtos.cs index ae1cef8..598e918 100644 --- a/src/NovelSoftware.Application/Dtos/BeatDtos.cs +++ b/src/Novelly.Api/Beats/BeatDtos.cs @@ -1,6 +1,6 @@ -using NovelSoftware.Domain.Entities; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Beats; public record BeatDto( Guid Id, diff --git a/src/NovelSoftware.Api/Endpoints/BeatEndpoints.cs b/src/Novelly.Api/Beats/BeatEndpoints.cs similarity index 94% rename from src/NovelSoftware.Api/Endpoints/BeatEndpoints.cs rename to src/Novelly.Api/Beats/BeatEndpoints.cs index 5e2fda1..bc2dd66 100644 --- a/src/NovelSoftware.Api/Endpoints/BeatEndpoints.cs +++ b/src/Novelly.Api/Beats/BeatEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Beats; public static class BeatEndpoints { diff --git a/src/NovelSoftware.Application/Services/BeatService.cs b/src/Novelly.Api/Beats/BeatService.cs similarity index 97% rename from src/NovelSoftware.Application/Services/BeatService.cs rename to src/Novelly.Api/Beats/BeatService.cs index 2f0d4e6..288047c 100644 --- a/src/NovelSoftware.Application/Services/BeatService.cs +++ b/src/Novelly.Api/Beats/BeatService.cs @@ -1,8 +1,10 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Chapters; +using Novelly.Api.Common; +using Novelly.Api.Data; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Services; +namespace Novelly.Api.Beats; /// /// Beats are a chapter's outline: a flat, ordered table rather than a tree. Everything diff --git a/src/NovelSoftware.Domain/Entities/Chapter.cs b/src/Novelly.Api/Chapters/Chapter.cs similarity index 87% rename from src/NovelSoftware.Domain/Entities/Chapter.cs rename to src/Novelly.Api/Chapters/Chapter.cs index 7f019bc..fb2f49a 100644 --- a/src/NovelSoftware.Domain/Entities/Chapter.cs +++ b/src/Novelly.Api/Chapters/Chapter.cs @@ -1,4 +1,11 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Beats; +using Novelly.Api.Characters; +using Novelly.Api.Common; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; + +namespace Novelly.Api.Chapters; /// A chapter: an ordered container of scenes plus its own planning fields. public class Chapter diff --git a/src/NovelSoftware.Application/Dtos/ChapterDtos.cs b/src/Novelly.Api/Chapters/ChapterDtos.cs similarity index 95% rename from src/NovelSoftware.Application/Dtos/ChapterDtos.cs rename to src/Novelly.Api/Chapters/ChapterDtos.cs index d32ed4f..0f3bf5c 100644 --- a/src/NovelSoftware.Application/Dtos/ChapterDtos.cs +++ b/src/Novelly.Api/Chapters/ChapterDtos.cs @@ -1,7 +1,9 @@ -using NovelSoftware.Domain; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Beats; +using Novelly.Api.Common; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Chapters; public record ChapterSummaryDto( Guid Id, diff --git a/src/NovelSoftware.Api/Endpoints/ChapterEndpoints.cs b/src/Novelly.Api/Chapters/ChapterEndpoints.cs similarity index 93% rename from src/NovelSoftware.Api/Endpoints/ChapterEndpoints.cs rename to src/Novelly.Api/Chapters/ChapterEndpoints.cs index 0140c44..a8178f2 100644 --- a/src/NovelSoftware.Api/Endpoints/ChapterEndpoints.cs +++ b/src/Novelly.Api/Chapters/ChapterEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Chapters; public static class ChapterEndpoints { diff --git a/src/NovelSoftware.Application/Services/ChapterService.cs b/src/Novelly.Api/Chapters/ChapterService.cs similarity index 96% rename from src/NovelSoftware.Application/Services/ChapterService.cs rename to src/Novelly.Api/Chapters/ChapterService.cs index bc3b879..9e9ed61 100644 --- a/src/NovelSoftware.Application/Services/ChapterService.cs +++ b/src/Novelly.Api/Chapters/ChapterService.cs @@ -1,8 +1,10 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Common; +using Novelly.Api.Data; +using Novelly.Api.Projects; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Services; +namespace Novelly.Api.Chapters; public class ChapterService(INovelDbContext db, TagService tags) { diff --git a/src/NovelSoftware.Domain/Entities/Character.cs b/src/Novelly.Api/Characters/Character.cs similarity index 96% rename from src/NovelSoftware.Domain/Entities/Character.cs rename to src/Novelly.Api/Characters/Character.cs index 8ccb3c3..943b49a 100644 --- a/src/NovelSoftware.Domain/Entities/Character.cs +++ b/src/Novelly.Api/Characters/Character.cs @@ -1,4 +1,7 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Projects; +using Novelly.Api.Tags; + +namespace Novelly.Api.Characters; /// /// A character dossier. Every field beyond is optional so a writer can diff --git a/src/NovelSoftware.Application/Dtos/CharacterDtos.cs b/src/Novelly.Api/Characters/CharacterDtos.cs similarity index 96% rename from src/NovelSoftware.Application/Dtos/CharacterDtos.cs rename to src/Novelly.Api/Characters/CharacterDtos.cs index 7ac61a5..f6e2d07 100644 --- a/src/NovelSoftware.Application/Dtos/CharacterDtos.cs +++ b/src/Novelly.Api/Characters/CharacterDtos.cs @@ -1,7 +1,6 @@ -using NovelSoftware.Domain; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Characters; public record CharacterDto( Guid Id, diff --git a/src/NovelSoftware.Api/Endpoints/CharacterEndpoints.cs b/src/Novelly.Api/Characters/CharacterEndpoints.cs similarity index 95% rename from src/NovelSoftware.Api/Endpoints/CharacterEndpoints.cs rename to src/Novelly.Api/Characters/CharacterEndpoints.cs index 0994eb2..9dda3df 100644 --- a/src/NovelSoftware.Api/Endpoints/CharacterEndpoints.cs +++ b/src/Novelly.Api/Characters/CharacterEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Characters; public static class CharacterEndpoints { diff --git a/src/Novelly.Api/Characters/CharacterRole.cs b/src/Novelly.Api/Characters/CharacterRole.cs new file mode 100644 index 0000000..7eb0049 --- /dev/null +++ b/src/Novelly.Api/Characters/CharacterRole.cs @@ -0,0 +1,14 @@ +namespace Novelly.Api.Characters; + +/// The role a character plays in the story. +public enum CharacterRole +{ + Protagonist, + Antagonist, + Deuteragonist, + Supporting, + Minor, + Mentor, + LoveInterest, + Foil +} diff --git a/src/NovelSoftware.Application/Services/CharacterService.cs b/src/Novelly.Api/Characters/CharacterService.cs similarity index 97% rename from src/NovelSoftware.Application/Services/CharacterService.cs rename to src/Novelly.Api/Characters/CharacterService.cs index 23b5665..fe2d599 100644 --- a/src/NovelSoftware.Application/Services/CharacterService.cs +++ b/src/Novelly.Api/Characters/CharacterService.cs @@ -1,8 +1,10 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Common; +using Novelly.Api.Data; +using Novelly.Api.Projects; +using Novelly.Api.Tags; -namespace NovelSoftware.Application.Services; +namespace Novelly.Api.Characters; public class CharacterService(INovelDbContext db, TagService tags) { diff --git a/src/NovelSoftware.Application/AgentNotConfiguredException.cs b/src/Novelly.Api/Common/AgentNotConfiguredException.cs similarity index 89% rename from src/NovelSoftware.Application/AgentNotConfiguredException.cs rename to src/Novelly.Api/Common/AgentNotConfiguredException.cs index fb72f0d..1ec2e05 100644 --- a/src/NovelSoftware.Application/AgentNotConfiguredException.cs +++ b/src/Novelly.Api/Common/AgentNotConfiguredException.cs @@ -1,4 +1,4 @@ -namespace NovelSoftware.Application; +namespace Novelly.Api.Common; /// /// Thrown when the agent is asked to run but has no model credentials. This is a diff --git a/src/Novelly.Api/Common/DraftStatus.cs b/src/Novelly.Api/Common/DraftStatus.cs new file mode 100644 index 0000000..07b7d5c --- /dev/null +++ b/src/Novelly.Api/Common/DraftStatus.cs @@ -0,0 +1,11 @@ +namespace Novelly.Api.Common; + +/// How far along a chapter or scene is in the drafting pipeline. +public enum DraftStatus +{ + Planned, + Outlined, + Drafted, + Revised, + Final +} diff --git a/src/NovelSoftware.Application/NotFoundException.cs b/src/Novelly.Api/Common/NotFoundException.cs similarity index 90% rename from src/NovelSoftware.Application/NotFoundException.cs rename to src/Novelly.Api/Common/NotFoundException.cs index 3130b07..dc5f9e1 100644 --- a/src/NovelSoftware.Application/NotFoundException.cs +++ b/src/Novelly.Api/Common/NotFoundException.cs @@ -1,4 +1,4 @@ -namespace NovelSoftware.Application; +namespace Novelly.Api.Common; /// /// Thrown when a service is asked for an entity that does not exist. The API translates diff --git a/src/NovelSoftware.Infrastructure/DependencyInjection.cs b/src/Novelly.Api/Common/NovellyServiceRegistration.cs similarity index 59% rename from src/NovelSoftware.Infrastructure/DependencyInjection.cs rename to src/Novelly.Api/Common/NovellyServiceRegistration.cs index b024735..2f753a5 100644 --- a/src/NovelSoftware.Infrastructure/DependencyInjection.cs +++ b/src/Novelly.Api/Common/NovellyServiceRegistration.cs @@ -1,17 +1,23 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using NovelSoftware.Application; -using NovelSoftware.Application.Agent; -using NovelSoftware.Application.Services; -using NovelSoftware.Infrastructure.Anthropic; -using NovelSoftware.Infrastructure.Persistence; +using Novelly.Api.Agent; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Data; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; -namespace NovelSoftware.Infrastructure; +namespace Novelly.Api.Common; -public static class DependencyInjection +/// +/// Wires up every feature's services in one place. Endpoints, the embedded agent and the +/// MCP server all resolve the same instances, so a capability added here is available to +/// all three. +/// +public static class NovellyServiceRegistration { - public static IServiceCollection AddNovelSoftware(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddNovelly(this IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString("Novel") ?? "Data Source=novel.db"; diff --git a/src/Novelly.Api/Common/Patch.cs b/src/Novelly.Api/Common/Patch.cs new file mode 100644 index 0000000..2839d88 --- /dev/null +++ b/src/Novelly.Api/Common/Patch.cs @@ -0,0 +1,15 @@ +namespace Novelly.Api.Common; + +/// +/// Patch semantics shared by every update endpoint: a null value leaves the field +/// untouched, an empty string clears it. +/// +internal static class Patch +{ + public static string? Apply(string? current, string? incoming) => incoming switch + { + null => current, + "" => null, + _ => incoming + }; +} diff --git a/src/NovelSoftware.Application/INovelDbContext.cs b/src/Novelly.Api/Data/INovelDbContext.cs similarity index 77% rename from src/NovelSoftware.Application/INovelDbContext.cs rename to src/Novelly.Api/Data/INovelDbContext.cs index abc9dd9..db25917 100644 --- a/src/NovelSoftware.Application/INovelDbContext.cs +++ b/src/Novelly.Api/Data/INovelDbContext.cs @@ -1,7 +1,13 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Agent; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; -namespace NovelSoftware.Application; +namespace Novelly.Api.Data; /// /// The persistence surface the application services depend on. Infrastructure supplies diff --git a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806023249_InitialSchema.Designer.cs b/src/Novelly.Api/Data/Migrations/20260806023249_InitialSchema.Designer.cs similarity index 84% rename from src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806023249_InitialSchema.Designer.cs rename to src/Novelly.Api/Data/Migrations/20260806023249_InitialSchema.Designer.cs index 2c8809c..5866962 100644 --- a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806023249_InitialSchema.Designer.cs +++ b/src/Novelly.Api/Data/Migrations/20260806023249_InitialSchema.Designer.cs @@ -2,13 +2,13 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Novelly.Api.Data; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NovelSoftware.Infrastructure.Persistence; #nullable disable -namespace NovelSoftware.Infrastructure.Persistence.Migrations +namespace Novelly.Api.Migrations { [DbContext(typeof(NovelDbContext))] [Migration("20260806023249_InitialSchema")] @@ -20,7 +20,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "10.0.10"); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -47,7 +47,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Conversations"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -82,7 +82,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("AgentMessages"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -134,7 +134,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Chapters"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -205,7 +205,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Characters"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b => + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -234,7 +234,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("CharacterRelationships"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.OutlineNode", b => + modelBuilder.Entity("Novelly.Api.Chapters.OutlineNode", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -282,7 +282,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("OutlineNodes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b => + modelBuilder.Entity("Novelly.Api.Projects.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -322,7 +322,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b => + modelBuilder.Entity("Novelly.Api.Scenes.Scene", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -383,9 +383,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Scenes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Conversations") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -394,9 +394,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => { - b.HasOne("NovelSoftware.Domain.Entities.AgentConversation", "Conversation") + b.HasOne("Novelly.Api.Agent.AgentConversation", "Conversation") .WithMany("Messages") .HasForeignKey("ConversationId") .OnDelete(DeleteBehavior.Cascade) @@ -405,14 +405,14 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Conversation"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter") + b.HasOne("Novelly.Api.Characters.Character", "PovCharacter") .WithMany() .HasForeignKey("PovCharacterId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Chapters") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -423,9 +423,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Characters") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -434,15 +434,15 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b => + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", "Character") + b.HasOne("Novelly.Api.Characters.Character", "Character") .WithMany("Relationships") .HasForeignKey("CharacterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "RelatedCharacter") + b.HasOne("Novelly.Api.Characters.Character", "RelatedCharacter") .WithMany() .HasForeignKey("RelatedCharacterId") .OnDelete(DeleteBehavior.Restrict) @@ -453,19 +453,19 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("RelatedCharacter"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.OutlineNode", b => + modelBuilder.Entity("Novelly.Api.Chapters.OutlineNode", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter") + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") .WithMany() .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("NovelSoftware.Domain.Entities.OutlineNode", "Parent") + b.HasOne("Novelly.Api.Chapters.OutlineNode", "Parent") .WithMany("Children") .HasForeignKey("ParentId") .OnDelete(DeleteBehavior.Restrict); - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("OutlineNodes") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -478,15 +478,15 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b => + modelBuilder.Entity("Novelly.Api.Scenes.Scene", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter") + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") .WithMany("Scenes") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter") + b.HasOne("Novelly.Api.Characters.Character", "PovCharacter") .WithMany() .HasForeignKey("PovCharacterId") .OnDelete(DeleteBehavior.SetNull); @@ -496,27 +496,27 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("PovCharacter"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { b.Navigation("Messages"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { b.Navigation("Scenes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { b.Navigation("Relationships"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.OutlineNode", b => + modelBuilder.Entity("Novelly.Api.Chapters.OutlineNode", b => { b.Navigation("Children"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b => + modelBuilder.Entity("Novelly.Api.Projects.Project", b => { b.Navigation("Chapters"); diff --git a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806023249_InitialSchema.cs b/src/Novelly.Api/Data/Migrations/20260806023249_InitialSchema.cs similarity index 99% rename from src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806023249_InitialSchema.cs rename to src/Novelly.Api/Data/Migrations/20260806023249_InitialSchema.cs index 026b6e6..8aa7c43 100644 --- a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806023249_InitialSchema.cs +++ b/src/Novelly.Api/Data/Migrations/20260806023249_InitialSchema.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace NovelSoftware.Infrastructure.Persistence.Migrations +namespace Novelly.Api.Migrations { /// public partial class InitialSchema : Migration diff --git a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs b/src/Novelly.Api/Data/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs similarity index 85% rename from src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs rename to src/Novelly.Api/Data/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs index 1fc78ee..0d8a658 100644 --- a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs +++ b/src/Novelly.Api/Data/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.Designer.cs @@ -2,13 +2,13 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Novelly.Api.Data; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NovelSoftware.Infrastructure.Persistence; #nullable disable -namespace NovelSoftware.Infrastructure.Persistence.Migrations +namespace Novelly.Api.Migrations { [DbContext(typeof(NovelDbContext))] [Migration("20260806031243_ReplaceOutlineWithBeatsAndTags")] @@ -65,7 +65,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("CharacterTags", (string)null); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -92,7 +92,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Conversations"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -127,7 +127,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("AgentMessages"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Beat", b => + modelBuilder.Entity("Novelly.Api.Beats.Beat", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -173,7 +173,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Beats"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -225,7 +225,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Chapters"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -296,7 +296,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Characters"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b => + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -325,7 +325,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("CharacterRelationships"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b => + modelBuilder.Entity("Novelly.Api.Projects.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -365,7 +365,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b => + modelBuilder.Entity("Novelly.Api.Scenes.Scene", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -426,7 +426,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Scenes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Tag", b => + modelBuilder.Entity("Novelly.Api.Tags.Tag", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -457,13 +457,13 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations modelBuilder.Entity("BeatTag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Beat", null) + b.HasOne("Novelly.Api.Beats.Beat", null) .WithMany() .HasForeignKey("BeatsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Tag", null) + b.HasOne("Novelly.Api.Tags.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) @@ -472,13 +472,13 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations modelBuilder.Entity("ChapterTag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", null) + b.HasOne("Novelly.Api.Chapters.Chapter", null) .WithMany() .HasForeignKey("ChaptersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Tag", null) + b.HasOne("Novelly.Api.Tags.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) @@ -487,22 +487,22 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations modelBuilder.Entity("CharacterTag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", null) + b.HasOne("Novelly.Api.Characters.Character", null) .WithMany() .HasForeignKey("CharactersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Tag", null) + b.HasOne("Novelly.Api.Tags.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Conversations") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -511,9 +511,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => { - b.HasOne("NovelSoftware.Domain.Entities.AgentConversation", "Conversation") + b.HasOne("Novelly.Api.Agent.AgentConversation", "Conversation") .WithMany("Messages") .HasForeignKey("ConversationId") .OnDelete(DeleteBehavior.Cascade) @@ -522,20 +522,20 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Conversation"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Beat", b => + modelBuilder.Entity("Novelly.Api.Beats.Beat", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter") + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") .WithMany("Beats") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "Character") + b.HasOne("Novelly.Api.Characters.Character", "Character") .WithMany() .HasForeignKey("CharacterId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("NovelSoftware.Domain.Entities.Scene", "Scene") + b.HasOne("Novelly.Api.Scenes.Scene", "Scene") .WithMany() .HasForeignKey("SceneId") .OnDelete(DeleteBehavior.SetNull); @@ -547,14 +547,14 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Scene"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter") + b.HasOne("Novelly.Api.Characters.Character", "PovCharacter") .WithMany() .HasForeignKey("PovCharacterId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Chapters") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -565,9 +565,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Characters") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -576,15 +576,15 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b => + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", "Character") + b.HasOne("Novelly.Api.Characters.Character", "Character") .WithMany("Relationships") .HasForeignKey("CharacterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "RelatedCharacter") + b.HasOne("Novelly.Api.Characters.Character", "RelatedCharacter") .WithMany() .HasForeignKey("RelatedCharacterId") .OnDelete(DeleteBehavior.Restrict) @@ -595,15 +595,15 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("RelatedCharacter"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b => + modelBuilder.Entity("Novelly.Api.Scenes.Scene", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter") + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") .WithMany("Scenes") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter") + b.HasOne("Novelly.Api.Characters.Character", "PovCharacter") .WithMany() .HasForeignKey("PovCharacterId") .OnDelete(DeleteBehavior.SetNull); @@ -613,9 +613,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("PovCharacter"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Tag", b => + modelBuilder.Entity("Novelly.Api.Tags.Tag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Tags") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -624,24 +624,24 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { b.Navigation("Messages"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { b.Navigation("Beats"); b.Navigation("Scenes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { b.Navigation("Relationships"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b => + modelBuilder.Entity("Novelly.Api.Projects.Project", b => { b.Navigation("Chapters"); diff --git a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs b/src/Novelly.Api/Data/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs similarity index 99% rename from src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs rename to src/Novelly.Api/Data/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs index 57c7e89..2cc6367 100644 --- a/src/NovelSoftware.Infrastructure/Persistence/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs +++ b/src/Novelly.Api/Data/Migrations/20260806031243_ReplaceOutlineWithBeatsAndTags.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace NovelSoftware.Infrastructure.Persistence.Migrations +namespace Novelly.Api.Migrations { /// public partial class ReplaceOutlineWithBeatsAndTags : Migration diff --git a/src/NovelSoftware.Infrastructure/Persistence/Migrations/NovelDbContextModelSnapshot.cs b/src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs similarity index 85% rename from src/NovelSoftware.Infrastructure/Persistence/Migrations/NovelDbContextModelSnapshot.cs rename to src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs index ffcc6ca..626b396 100644 --- a/src/NovelSoftware.Infrastructure/Persistence/Migrations/NovelDbContextModelSnapshot.cs +++ b/src/Novelly.Api/Data/Migrations/NovelDbContextModelSnapshot.cs @@ -2,12 +2,12 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Novelly.Api.Data; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NovelSoftware.Infrastructure.Persistence; #nullable disable -namespace NovelSoftware.Infrastructure.Persistence.Migrations +namespace Novelly.Api.Migrations { [DbContext(typeof(NovelDbContext))] partial class NovelDbContextModelSnapshot : ModelSnapshot @@ -62,7 +62,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("CharacterTags", (string)null); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -89,7 +89,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Conversations"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -124,7 +124,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("AgentMessages"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Beat", b => + modelBuilder.Entity("Novelly.Api.Beats.Beat", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -170,7 +170,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Beats"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -222,7 +222,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Chapters"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -293,7 +293,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Characters"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b => + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -322,7 +322,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("CharacterRelationships"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b => + modelBuilder.Entity("Novelly.Api.Projects.Project", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -362,7 +362,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Projects"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b => + modelBuilder.Entity("Novelly.Api.Scenes.Scene", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -423,7 +423,7 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.ToTable("Scenes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Tag", b => + modelBuilder.Entity("Novelly.Api.Tags.Tag", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -454,13 +454,13 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations modelBuilder.Entity("BeatTag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Beat", null) + b.HasOne("Novelly.Api.Beats.Beat", null) .WithMany() .HasForeignKey("BeatsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Tag", null) + b.HasOne("Novelly.Api.Tags.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) @@ -469,13 +469,13 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations modelBuilder.Entity("ChapterTag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", null) + b.HasOne("Novelly.Api.Chapters.Chapter", null) .WithMany() .HasForeignKey("ChaptersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Tag", null) + b.HasOne("Novelly.Api.Tags.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) @@ -484,22 +484,22 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations modelBuilder.Entity("CharacterTag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", null) + b.HasOne("Novelly.Api.Characters.Character", null) .WithMany() .HasForeignKey("CharactersId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Tag", null) + b.HasOne("Novelly.Api.Tags.Tag", null) .WithMany() .HasForeignKey("TagsId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Conversations") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -508,9 +508,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b => { - b.HasOne("NovelSoftware.Domain.Entities.AgentConversation", "Conversation") + b.HasOne("Novelly.Api.Agent.AgentConversation", "Conversation") .WithMany("Messages") .HasForeignKey("ConversationId") .OnDelete(DeleteBehavior.Cascade) @@ -519,20 +519,20 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Conversation"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Beat", b => + modelBuilder.Entity("Novelly.Api.Beats.Beat", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter") + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") .WithMany("Beats") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "Character") + b.HasOne("Novelly.Api.Characters.Character", "Character") .WithMany() .HasForeignKey("CharacterId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("NovelSoftware.Domain.Entities.Scene", "Scene") + b.HasOne("Novelly.Api.Scenes.Scene", "Scene") .WithMany() .HasForeignKey("SceneId") .OnDelete(DeleteBehavior.SetNull); @@ -544,14 +544,14 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Scene"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter") + b.HasOne("Novelly.Api.Characters.Character", "PovCharacter") .WithMany() .HasForeignKey("PovCharacterId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Chapters") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -562,9 +562,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Characters") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -573,15 +573,15 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b => + modelBuilder.Entity("Novelly.Api.Characters.CharacterRelationship", b => { - b.HasOne("NovelSoftware.Domain.Entities.Character", "Character") + b.HasOne("Novelly.Api.Characters.Character", "Character") .WithMany("Relationships") .HasForeignKey("CharacterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "RelatedCharacter") + b.HasOne("Novelly.Api.Characters.Character", "RelatedCharacter") .WithMany() .HasForeignKey("RelatedCharacterId") .OnDelete(DeleteBehavior.Restrict) @@ -592,15 +592,15 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("RelatedCharacter"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b => + modelBuilder.Entity("Novelly.Api.Scenes.Scene", b => { - b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter") + b.HasOne("Novelly.Api.Chapters.Chapter", "Chapter") .WithMany("Scenes") .HasForeignKey("ChapterId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter") + b.HasOne("Novelly.Api.Characters.Character", "PovCharacter") .WithMany() .HasForeignKey("PovCharacterId") .OnDelete(DeleteBehavior.SetNull); @@ -610,9 +610,9 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("PovCharacter"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Tag", b => + modelBuilder.Entity("Novelly.Api.Tags.Tag", b => { - b.HasOne("NovelSoftware.Domain.Entities.Project", "Project") + b.HasOne("Novelly.Api.Projects.Project", "Project") .WithMany("Tags") .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade) @@ -621,24 +621,24 @@ namespace NovelSoftware.Infrastructure.Persistence.Migrations b.Navigation("Project"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b => + modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b => { b.Navigation("Messages"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b => + modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b => { b.Navigation("Beats"); b.Navigation("Scenes"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b => + modelBuilder.Entity("Novelly.Api.Characters.Character", b => { b.Navigation("Relationships"); }); - modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b => + modelBuilder.Entity("Novelly.Api.Projects.Project", b => { b.Navigation("Chapters"); diff --git a/src/NovelSoftware.Infrastructure/Persistence/NovelDbContext.cs b/src/Novelly.Api/Data/NovelDbContext.cs similarity index 96% rename from src/NovelSoftware.Infrastructure/Persistence/NovelDbContext.cs rename to src/Novelly.Api/Data/NovelDbContext.cs index c15611d..07cf092 100644 --- a/src/NovelSoftware.Infrastructure/Persistence/NovelDbContext.cs +++ b/src/Novelly.Api/Data/NovelDbContext.cs @@ -1,9 +1,14 @@ -using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NovelSoftware.Application; -using NovelSoftware.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Novelly.Api.Agent; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; -namespace NovelSoftware.Infrastructure.Persistence; +namespace Novelly.Api.Data; /// /// Stores a as UTC ticks. SQLite has no native type for it diff --git a/src/NovelSoftware.Infrastructure/NovelSoftware.Infrastructure.csproj b/src/Novelly.Api/Novelly.Api.csproj similarity index 72% rename from src/NovelSoftware.Infrastructure/NovelSoftware.Infrastructure.csproj rename to src/Novelly.Api/Novelly.Api.csproj index 376ba3f..1840e53 100644 --- a/src/NovelSoftware.Infrastructure/NovelSoftware.Infrastructure.csproj +++ b/src/Novelly.Api/Novelly.Api.csproj @@ -1,25 +1,26 @@ - + - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + net10.0 - enable enable latest + enable diff --git a/src/NovelSoftware.Api/Program.cs b/src/Novelly.Api/Program.cs similarity index 87% rename from src/NovelSoftware.Api/Program.cs rename to src/Novelly.Api/Program.cs index 5752c4a..9670100 100644 --- a/src/NovelSoftware.Api/Program.cs +++ b/src/Novelly.Api/Program.cs @@ -1,14 +1,20 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.EntityFrameworkCore; -using NovelSoftware.Api.Endpoints; -using NovelSoftware.Application; -using NovelSoftware.Infrastructure; -using NovelSoftware.Infrastructure.Persistence; +using Novelly.Api.Agent; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Common; +using Novelly.Api.Data; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddNovelSoftware(builder.Configuration); +builder.AddServiceDefaults(); +builder.Services.AddNovelly(builder.Configuration); builder.Services.AddOpenApi(); builder.Services.AddProblemDetails(); @@ -63,6 +69,8 @@ if (app.Environment.IsDevelopment()) app.MapOpenApi(); } +app.MapDefaultEndpoints(); + app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })).WithTags("Health"); app.MapProjectEndpoints() diff --git a/src/NovelSoftware.Domain/Entities/Project.cs b/src/Novelly.Api/Projects/Project.cs similarity index 88% rename from src/NovelSoftware.Domain/Entities/Project.cs rename to src/Novelly.Api/Projects/Project.cs index 44f174a..d08f746 100644 --- a/src/NovelSoftware.Domain/Entities/Project.cs +++ b/src/Novelly.Api/Projects/Project.cs @@ -1,4 +1,9 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Agent; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Tags; + +namespace Novelly.Api.Projects; /// A single novel and everything that belongs to it. public class Project diff --git a/src/NovelSoftware.Application/Dtos/ProjectDtos.cs b/src/Novelly.Api/Projects/ProjectDtos.cs similarity index 94% rename from src/NovelSoftware.Application/Dtos/ProjectDtos.cs rename to src/Novelly.Api/Projects/ProjectDtos.cs index ebe4d59..abd5bde 100644 --- a/src/NovelSoftware.Application/Dtos/ProjectDtos.cs +++ b/src/Novelly.Api/Projects/ProjectDtos.cs @@ -1,6 +1,4 @@ -using NovelSoftware.Domain.Entities; - -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Projects; public record ProjectSummaryDto( Guid Id, diff --git a/src/NovelSoftware.Api/Endpoints/ProjectEndpoints.cs b/src/Novelly.Api/Projects/ProjectEndpoints.cs similarity index 92% rename from src/NovelSoftware.Api/Endpoints/ProjectEndpoints.cs rename to src/Novelly.Api/Projects/ProjectEndpoints.cs index e093d4b..3ae99b2 100644 --- a/src/NovelSoftware.Api/Endpoints/ProjectEndpoints.cs +++ b/src/Novelly.Api/Projects/ProjectEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Projects; public static class ProjectEndpoints { diff --git a/src/NovelSoftware.Application/Services/ProjectService.cs b/src/Novelly.Api/Projects/ProjectService.cs similarity index 84% rename from src/NovelSoftware.Application/Services/ProjectService.cs rename to src/Novelly.Api/Projects/ProjectService.cs index 213dc71..b4f7da4 100644 --- a/src/NovelSoftware.Application/Services/ProjectService.cs +++ b/src/Novelly.Api/Projects/ProjectService.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Common; +using Novelly.Api.Data; -namespace NovelSoftware.Application.Services; +namespace Novelly.Api.Projects; public class ProjectService(INovelDbContext db) { @@ -71,17 +71,3 @@ public class ProjectService(INovelDbContext db) await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct) ?? throw new NotFoundException(nameof(Project), id); } - -/// -/// Patch semantics shared by every update endpoint: a null value leaves the field -/// untouched, an empty string clears it. -/// -internal static class Patch -{ - public static string? Apply(string? current, string? incoming) => incoming switch - { - null => current, - "" => null, - _ => incoming - }; -} diff --git a/src/NovelSoftware.Api/Properties/launchSettings.json b/src/Novelly.Api/Properties/launchSettings.json similarity index 100% rename from src/NovelSoftware.Api/Properties/launchSettings.json rename to src/Novelly.Api/Properties/launchSettings.json diff --git a/src/NovelSoftware.Domain/Entities/Scene.cs b/src/Novelly.Api/Scenes/Scene.cs similarity index 91% rename from src/NovelSoftware.Domain/Entities/Scene.cs rename to src/Novelly.Api/Scenes/Scene.cs index 9acaba9..6b3eaa4 100644 --- a/src/NovelSoftware.Domain/Entities/Scene.cs +++ b/src/Novelly.Api/Scenes/Scene.cs @@ -1,4 +1,8 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Common; + +namespace Novelly.Api.Scenes; /// /// A scene inside a chapter. The goal/conflict/outcome trio is the unit the agent diff --git a/src/NovelSoftware.Application/Dtos/SceneDtos.cs b/src/Novelly.Api/Scenes/SceneDtos.cs similarity index 93% rename from src/NovelSoftware.Application/Dtos/SceneDtos.cs rename to src/Novelly.Api/Scenes/SceneDtos.cs index 6aa873c..7d94204 100644 --- a/src/NovelSoftware.Application/Dtos/SceneDtos.cs +++ b/src/Novelly.Api/Scenes/SceneDtos.cs @@ -1,7 +1,6 @@ -using NovelSoftware.Domain; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Common; -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Scenes; public record SceneDto( Guid Id, diff --git a/src/NovelSoftware.Api/Endpoints/SceneEndpoints.cs b/src/Novelly.Api/Scenes/SceneEndpoints.cs similarity index 93% rename from src/NovelSoftware.Api/Endpoints/SceneEndpoints.cs rename to src/Novelly.Api/Scenes/SceneEndpoints.cs index 13ca5a3..424b159 100644 --- a/src/NovelSoftware.Api/Endpoints/SceneEndpoints.cs +++ b/src/Novelly.Api/Scenes/SceneEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Scenes; public static class SceneEndpoints { diff --git a/src/NovelSoftware.Application/Services/SceneService.cs b/src/Novelly.Api/Scenes/SceneService.cs similarity index 96% rename from src/NovelSoftware.Application/Services/SceneService.cs rename to src/Novelly.Api/Scenes/SceneService.cs index 3b49ba8..76e7d7c 100644 --- a/src/NovelSoftware.Application/Services/SceneService.cs +++ b/src/Novelly.Api/Scenes/SceneService.cs @@ -1,8 +1,9 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Chapters; +using Novelly.Api.Common; +using Novelly.Api.Data; -namespace NovelSoftware.Application.Services; +namespace Novelly.Api.Scenes; public class SceneService(INovelDbContext db) { diff --git a/src/NovelSoftware.Domain/Entities/Tag.cs b/src/Novelly.Api/Tags/Tag.cs similarity index 85% rename from src/NovelSoftware.Domain/Entities/Tag.cs rename to src/Novelly.Api/Tags/Tag.cs index c7e4ab8..e9ec187 100644 --- a/src/NovelSoftware.Domain/Entities/Tag.cs +++ b/src/Novelly.Api/Tags/Tag.cs @@ -1,4 +1,9 @@ -namespace NovelSoftware.Domain.Entities; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Projects; + +namespace Novelly.Api.Tags; /// /// A free-form label scoped to one project. Tags are the cross-reference mechanism: diff --git a/src/NovelSoftware.Application/Dtos/TagDtos.cs b/src/Novelly.Api/Tags/TagDtos.cs similarity index 95% rename from src/NovelSoftware.Application/Dtos/TagDtos.cs rename to src/Novelly.Api/Tags/TagDtos.cs index 47fed27..e2d0171 100644 --- a/src/NovelSoftware.Application/Dtos/TagDtos.cs +++ b/src/Novelly.Api/Tags/TagDtos.cs @@ -1,6 +1,4 @@ -using NovelSoftware.Domain.Entities; - -namespace NovelSoftware.Application.Dtos; +namespace Novelly.Api.Tags; public record TagDto(Guid Id, string Name, string? Color); diff --git a/src/NovelSoftware.Api/Endpoints/TagEndpoints.cs b/src/Novelly.Api/Tags/TagEndpoints.cs similarity index 93% rename from src/NovelSoftware.Api/Endpoints/TagEndpoints.cs rename to src/Novelly.Api/Tags/TagEndpoints.cs index ba9877b..f4f5af1 100644 --- a/src/NovelSoftware.Api/Endpoints/TagEndpoints.cs +++ b/src/Novelly.Api/Tags/TagEndpoints.cs @@ -1,7 +1,4 @@ -using NovelSoftware.Application.Dtos; -using NovelSoftware.Application.Services; - -namespace NovelSoftware.Api.Endpoints; +namespace Novelly.Api.Tags; public static class TagEndpoints { diff --git a/src/NovelSoftware.Application/Services/TagService.cs b/src/Novelly.Api/Tags/TagService.cs similarity index 97% rename from src/NovelSoftware.Application/Services/TagService.cs rename to src/Novelly.Api/Tags/TagService.cs index 06cc927..c2caf97 100644 --- a/src/NovelSoftware.Application/Services/TagService.cs +++ b/src/Novelly.Api/Tags/TagService.cs @@ -1,8 +1,9 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain.Entities; +using Novelly.Api.Common; +using Novelly.Api.Data; +using Novelly.Api.Projects; -namespace NovelSoftware.Application.Services; +namespace Novelly.Api.Tags; public class TagService(INovelDbContext db) { diff --git a/src/NovelSoftware.Api/appsettings.Development.json b/src/Novelly.Api/appsettings.Development.json similarity index 78% rename from src/NovelSoftware.Api/appsettings.Development.json rename to src/Novelly.Api/appsettings.Development.json index 36ce91e..5997abb 100644 --- a/src/NovelSoftware.Api/appsettings.Development.json +++ b/src/Novelly.Api/appsettings.Development.json @@ -2,7 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "NovelSoftware": "Debug", + "Novelly": "Debug", "Microsoft.AspNetCore": "Warning" } } diff --git a/src/NovelSoftware.Api/appsettings.json b/src/Novelly.Api/appsettings.json similarity index 100% rename from src/NovelSoftware.Api/appsettings.json rename to src/Novelly.Api/appsettings.json diff --git a/src/Novelly.AppHost/AppHost.cs b/src/Novelly.AppHost/AppHost.cs new file mode 100644 index 0000000..1155037 --- /dev/null +++ b/src/Novelly.AppHost/AppHost.cs @@ -0,0 +1,15 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// Port 5080 is pinned to match src/Novelly.Web's Vite proxy default and the curl-based +// smoke checks in CLAUDE.md, so the API sits at the same address whether it is started +// on its own with `dotnet run` or through this AppHost. +var api = builder.AddProject("api") + .WithHttpEndpoint(port: 5080, name: "http"); + +builder.AddViteApp("web", "../Novelly.Web", "dev") + .WithReference(api) + .WaitFor(api) + .WithHttpEndpoint(port: 5173, name: "http") + .WithExternalHttpEndpoints(); + +builder.Build().Run(); diff --git a/src/Novelly.AppHost/Novelly.AppHost.csproj b/src/Novelly.AppHost/Novelly.AppHost.csproj new file mode 100644 index 0000000..40f144b --- /dev/null +++ b/src/Novelly.AppHost/Novelly.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + Exe + net10.0 + enable + enable + latest + true + 4f9d2a13-6c85-4b0e-9a7c-2d3f18b6e0c4 + + + diff --git a/src/Novelly.AppHost/Properties/launchSettings.json b/src/Novelly.AppHost/Properties/launchSettings.json new file mode 100755 index 0000000..3494766 --- /dev/null +++ b/src/Novelly.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17244;http://localhost:15050", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21229", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22051" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15050", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19033", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20209" + } + } + } +} diff --git a/src/Novelly.AppHost/appsettings.Development.json b/src/Novelly.AppHost/appsettings.Development.json new file mode 100755 index 0000000..57715f5 --- /dev/null +++ b/src/Novelly.AppHost/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Dashboard": { + "Frontend": { + "AuthMode": "Unsecured" + }, + "Otlp": { + "AuthMode": "Unsecured" + } + } +} diff --git a/src/Novelly.AppHost/appsettings.json b/src/Novelly.AppHost/appsettings.json new file mode 100755 index 0000000..2185f95 --- /dev/null +++ b/src/Novelly.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/Novelly.AppHost/aspire.config.json b/src/Novelly.AppHost/aspire.config.json new file mode 100755 index 0000000..12a8da5 --- /dev/null +++ b/src/Novelly.AppHost/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "Novelly.AppHost.csproj" + } +} diff --git a/src/NovelSoftware.Mcp/NovelApiClient.cs b/src/Novelly.Mcp/NovelApiClient.cs similarity index 93% rename from src/NovelSoftware.Mcp/NovelApiClient.cs rename to src/Novelly.Mcp/NovelApiClient.cs index b35be8b..88d7bd0 100644 --- a/src/NovelSoftware.Mcp/NovelApiClient.cs +++ b/src/Novelly.Mcp/NovelApiClient.cs @@ -3,10 +3,10 @@ using System.Net.Http.Json; using System.Text.Json; using ModelContextProtocol.Protocol; -namespace NovelSoftware.Mcp; +namespace Novelly.Mcp; /// -/// Thin wrapper over the NovelSoftware REST API. The MCP server deliberately owns no +/// Thin wrapper over the Novelly REST API. The MCP server deliberately owns no /// domain logic of its own — it is a second front end onto the same API the web client /// uses, so an edit made from Claude Code and one made in the browser are the same edit. /// @@ -51,7 +51,7 @@ public class NovelApiClient(HttpClient http) { // The API not being up is the most common failure here, and a bare connection // exception tells the model nothing actionable. - return Error($"Could not reach the NovelSoftware API at {http.BaseAddress}. Is it running? ({ex.Message})"); + return Error($"Could not reach the Novelly API at {http.BaseAddress}. Is it running? ({ex.Message})"); } var body = await response.Content.ReadAsStringAsync(ct); diff --git a/src/NovelSoftware.Mcp/NovelSoftware.Mcp.csproj b/src/Novelly.Mcp/Novelly.Mcp.csproj similarity index 100% rename from src/NovelSoftware.Mcp/NovelSoftware.Mcp.csproj rename to src/Novelly.Mcp/Novelly.Mcp.csproj diff --git a/src/NovelSoftware.Mcp/Program.cs b/src/Novelly.Mcp/Program.cs similarity index 87% rename from src/NovelSoftware.Mcp/Program.cs rename to src/Novelly.Mcp/Program.cs index dc3f351..5995a43 100644 --- a/src/NovelSoftware.Mcp/Program.cs +++ b/src/Novelly.Mcp/Program.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using NovelSoftware.Mcp; +using Novelly.Mcp; var builder = Host.CreateApplicationBuilder(args); @@ -11,7 +11,7 @@ builder.Logging.ClearProviders(); builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace); builder.Logging.SetMinimumLevel(LogLevel.Warning); -var apiBaseUrl = builder.Configuration["NOVELSOFTWARE_API_URL"] ?? "http://localhost:5080"; +var apiBaseUrl = builder.Configuration["NOVELLY_API_URL"] ?? "http://localhost:5080"; builder.Services.AddHttpClient(client => { diff --git a/src/NovelSoftware.Mcp/Tools/BeatTools.cs b/src/Novelly.Mcp/Tools/BeatTools.cs similarity index 99% rename from src/NovelSoftware.Mcp/Tools/BeatTools.cs rename to src/Novelly.Mcp/Tools/BeatTools.cs index 1068ae3..028eb52 100644 --- a/src/NovelSoftware.Mcp/Tools/BeatTools.cs +++ b/src/Novelly.Mcp/Tools/BeatTools.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace NovelSoftware.Mcp.Tools; +namespace Novelly.Mcp.Tools; [McpServerToolType] public static class BeatTools diff --git a/src/NovelSoftware.Mcp/Tools/CharacterTools.cs b/src/Novelly.Mcp/Tools/CharacterTools.cs similarity index 99% rename from src/NovelSoftware.Mcp/Tools/CharacterTools.cs rename to src/Novelly.Mcp/Tools/CharacterTools.cs index a9367a1..194097b 100644 --- a/src/NovelSoftware.Mcp/Tools/CharacterTools.cs +++ b/src/Novelly.Mcp/Tools/CharacterTools.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace NovelSoftware.Mcp.Tools; +namespace Novelly.Mcp.Tools; [McpServerToolType] public static class CharacterTools diff --git a/src/NovelSoftware.Mcp/Tools/ManuscriptTools.cs b/src/Novelly.Mcp/Tools/ManuscriptTools.cs similarity index 99% rename from src/NovelSoftware.Mcp/Tools/ManuscriptTools.cs rename to src/Novelly.Mcp/Tools/ManuscriptTools.cs index 7062009..40f7b5e 100644 --- a/src/NovelSoftware.Mcp/Tools/ManuscriptTools.cs +++ b/src/Novelly.Mcp/Tools/ManuscriptTools.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace NovelSoftware.Mcp.Tools; +namespace Novelly.Mcp.Tools; [McpServerToolType] public static class ManuscriptTools diff --git a/src/NovelSoftware.Mcp/Tools/ProjectTools.cs b/src/Novelly.Mcp/Tools/ProjectTools.cs similarity index 98% rename from src/NovelSoftware.Mcp/Tools/ProjectTools.cs rename to src/Novelly.Mcp/Tools/ProjectTools.cs index 7756225..3545612 100644 --- a/src/NovelSoftware.Mcp/Tools/ProjectTools.cs +++ b/src/Novelly.Mcp/Tools/ProjectTools.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace NovelSoftware.Mcp.Tools; +namespace Novelly.Mcp.Tools; [McpServerToolType] public static class ProjectTools diff --git a/src/NovelSoftware.Mcp/Tools/TagTools.cs b/src/Novelly.Mcp/Tools/TagTools.cs similarity index 98% rename from src/NovelSoftware.Mcp/Tools/TagTools.cs rename to src/Novelly.Mcp/Tools/TagTools.cs index cf8672e..07a09e8 100644 --- a/src/NovelSoftware.Mcp/Tools/TagTools.cs +++ b/src/Novelly.Mcp/Tools/TagTools.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace NovelSoftware.Mcp.Tools; +namespace Novelly.Mcp.Tools; [McpServerToolType] public static class TagTools diff --git a/src/Novelly.ServiceDefaults/Extensions.cs b/src/Novelly.ServiceDefaults/Extensions.cs new file mode 100755 index 0000000..97642a1 --- /dev/null +++ b/src/Novelly.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/Novelly.ServiceDefaults/Novelly.ServiceDefaults.csproj b/src/Novelly.ServiceDefaults/Novelly.ServiceDefaults.csproj new file mode 100644 index 0000000..aa0ed69 --- /dev/null +++ b/src/Novelly.ServiceDefaults/Novelly.ServiceDefaults.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + latest + true + true + + + + + + + + + + + + + + + diff --git a/src/NovelSoftware.Web/.gitignore b/src/Novelly.Web/.gitignore similarity index 100% rename from src/NovelSoftware.Web/.gitignore rename to src/Novelly.Web/.gitignore diff --git a/src/NovelSoftware.Web/.oxlintrc.json b/src/Novelly.Web/.oxlintrc.json similarity index 100% rename from src/NovelSoftware.Web/.oxlintrc.json rename to src/Novelly.Web/.oxlintrc.json diff --git a/src/NovelSoftware.Web/README.md b/src/Novelly.Web/README.md similarity index 100% rename from src/NovelSoftware.Web/README.md rename to src/Novelly.Web/README.md diff --git a/src/NovelSoftware.Web/index.html b/src/Novelly.Web/index.html similarity index 100% rename from src/NovelSoftware.Web/index.html rename to src/Novelly.Web/index.html diff --git a/src/NovelSoftware.Web/package-lock.json b/src/Novelly.Web/package-lock.json similarity index 99% rename from src/NovelSoftware.Web/package-lock.json rename to src/Novelly.Web/package-lock.json index 517bb43..a9103ac 100644 --- a/src/NovelSoftware.Web/package-lock.json +++ b/src/Novelly.Web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "novelsoftware-web", + "name": "novelly-web", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "novelsoftware-web", + "name": "novelly-web", "version": "0.0.0", "dependencies": { "@tanstack/react-query": "^5.101.4", diff --git a/src/NovelSoftware.Web/package.json b/src/Novelly.Web/package.json similarity index 95% rename from src/NovelSoftware.Web/package.json rename to src/Novelly.Web/package.json index dacf4ac..cbbda13 100644 --- a/src/NovelSoftware.Web/package.json +++ b/src/Novelly.Web/package.json @@ -1,5 +1,5 @@ { - "name": "novelsoftware-web", + "name": "novelly-web", "private": true, "version": "0.0.0", "type": "module", diff --git a/src/NovelSoftware.Web/public/favicon.svg b/src/Novelly.Web/public/favicon.svg similarity index 100% rename from src/NovelSoftware.Web/public/favicon.svg rename to src/Novelly.Web/public/favicon.svg diff --git a/src/NovelSoftware.Web/public/icons.svg b/src/Novelly.Web/public/icons.svg similarity index 100% rename from src/NovelSoftware.Web/public/icons.svg rename to src/Novelly.Web/public/icons.svg diff --git a/src/NovelSoftware.Web/src/App.tsx b/src/Novelly.Web/src/App.tsx similarity index 100% rename from src/NovelSoftware.Web/src/App.tsx rename to src/Novelly.Web/src/App.tsx diff --git a/src/NovelSoftware.Web/src/api/client.ts b/src/Novelly.Web/src/api/client.ts similarity index 100% rename from src/NovelSoftware.Web/src/api/client.ts rename to src/Novelly.Web/src/api/client.ts diff --git a/src/NovelSoftware.Web/src/api/hooks.ts b/src/Novelly.Web/src/api/hooks.ts similarity index 100% rename from src/NovelSoftware.Web/src/api/hooks.ts rename to src/Novelly.Web/src/api/hooks.ts diff --git a/src/NovelSoftware.Web/src/api/types.ts b/src/Novelly.Web/src/api/types.ts similarity index 97% rename from src/NovelSoftware.Web/src/api/types.ts rename to src/Novelly.Web/src/api/types.ts index 7489ff6..ddc4047 100644 --- a/src/NovelSoftware.Web/src/api/types.ts +++ b/src/Novelly.Web/src/api/types.ts @@ -1,4 +1,4 @@ -// Mirrors the DTOs in NovelSoftware.Application.Dtos. Enums travel as their names. +// Mirrors the DTOs in the Novelly.Api feature folders. Enums travel as their names. export type CharacterRole = | 'Protagonist' diff --git a/src/NovelSoftware.Web/src/components/TagEditor.tsx b/src/Novelly.Web/src/components/TagEditor.tsx similarity index 100% rename from src/NovelSoftware.Web/src/components/TagEditor.tsx rename to src/Novelly.Web/src/components/TagEditor.tsx diff --git a/src/NovelSoftware.Web/src/components/ui.tsx b/src/Novelly.Web/src/components/ui.tsx similarity index 100% rename from src/NovelSoftware.Web/src/components/ui.tsx rename to src/Novelly.Web/src/components/ui.tsx diff --git a/src/NovelSoftware.Web/src/index.css b/src/Novelly.Web/src/index.css similarity index 100% rename from src/NovelSoftware.Web/src/index.css rename to src/Novelly.Web/src/index.css diff --git a/src/NovelSoftware.Web/src/main.tsx b/src/Novelly.Web/src/main.tsx similarity index 100% rename from src/NovelSoftware.Web/src/main.tsx rename to src/Novelly.Web/src/main.tsx diff --git a/src/NovelSoftware.Web/src/pages/AgentPage.tsx b/src/Novelly.Web/src/pages/AgentPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/AgentPage.tsx rename to src/Novelly.Web/src/pages/AgentPage.tsx diff --git a/src/NovelSoftware.Web/src/pages/ChapterPage.tsx b/src/Novelly.Web/src/pages/ChapterPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/ChapterPage.tsx rename to src/Novelly.Web/src/pages/ChapterPage.tsx diff --git a/src/NovelSoftware.Web/src/pages/ChaptersPage.tsx b/src/Novelly.Web/src/pages/ChaptersPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/ChaptersPage.tsx rename to src/Novelly.Web/src/pages/ChaptersPage.tsx diff --git a/src/NovelSoftware.Web/src/pages/CharactersPage.tsx b/src/Novelly.Web/src/pages/CharactersPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/CharactersPage.tsx rename to src/Novelly.Web/src/pages/CharactersPage.tsx diff --git a/src/NovelSoftware.Web/src/pages/OverviewPage.tsx b/src/Novelly.Web/src/pages/OverviewPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/OverviewPage.tsx rename to src/Novelly.Web/src/pages/OverviewPage.tsx diff --git a/src/NovelSoftware.Web/src/pages/ProjectLayout.tsx b/src/Novelly.Web/src/pages/ProjectLayout.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/ProjectLayout.tsx rename to src/Novelly.Web/src/pages/ProjectLayout.tsx diff --git a/src/NovelSoftware.Web/src/pages/ProjectsPage.tsx b/src/Novelly.Web/src/pages/ProjectsPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/ProjectsPage.tsx rename to src/Novelly.Web/src/pages/ProjectsPage.tsx diff --git a/src/NovelSoftware.Web/src/pages/TagsPage.tsx b/src/Novelly.Web/src/pages/TagsPage.tsx similarity index 100% rename from src/NovelSoftware.Web/src/pages/TagsPage.tsx rename to src/Novelly.Web/src/pages/TagsPage.tsx diff --git a/src/NovelSoftware.Web/src/vite-env.d.ts b/src/Novelly.Web/src/vite-env.d.ts similarity index 100% rename from src/NovelSoftware.Web/src/vite-env.d.ts rename to src/Novelly.Web/src/vite-env.d.ts diff --git a/src/NovelSoftware.Web/tsconfig.app.json b/src/Novelly.Web/tsconfig.app.json similarity index 100% rename from src/NovelSoftware.Web/tsconfig.app.json rename to src/Novelly.Web/tsconfig.app.json diff --git a/src/NovelSoftware.Web/tsconfig.json b/src/Novelly.Web/tsconfig.json similarity index 100% rename from src/NovelSoftware.Web/tsconfig.json rename to src/Novelly.Web/tsconfig.json diff --git a/src/NovelSoftware.Web/tsconfig.node.json b/src/Novelly.Web/tsconfig.node.json similarity index 100% rename from src/NovelSoftware.Web/tsconfig.node.json rename to src/Novelly.Web/tsconfig.node.json diff --git a/src/NovelSoftware.Web/vite.config.ts b/src/Novelly.Web/vite.config.ts similarity index 100% rename from src/NovelSoftware.Web/vite.config.ts rename to src/Novelly.Web/vite.config.ts diff --git a/tests/NovelSoftware.Tests/AnthropicClientTests.cs b/tests/Novelly.Api.Tests/AnthropicClientTests.cs similarity index 89% rename from tests/NovelSoftware.Tests/AnthropicClientTests.cs rename to tests/Novelly.Api.Tests/AnthropicClientTests.cs index 8c05d53..70430af 100644 --- a/tests/NovelSoftware.Tests/AnthropicClientTests.cs +++ b/tests/Novelly.Api.Tests/AnthropicClientTests.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Options; -using NovelSoftware.Application; -using NovelSoftware.Application.Agent; -using NovelSoftware.Infrastructure.Anthropic; +using Novelly.Api.Agent; +using Novelly.Api.Common; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; [TestFixture] public class AnthropicClientTests diff --git a/tests/NovelSoftware.Tests/BeatServiceTests.cs b/tests/Novelly.Api.Tests/BeatServiceTests.cs similarity index 97% rename from tests/NovelSoftware.Tests/BeatServiceTests.cs rename to tests/Novelly.Api.Tests/BeatServiceTests.cs index d0e9db9..a3e0cec 100644 --- a/tests/NovelSoftware.Tests/BeatServiceTests.cs +++ b/tests/Novelly.Api.Tests/BeatServiceTests.cs @@ -1,7 +1,11 @@ -using NovelSoftware.Application; -using NovelSoftware.Application.Dtos; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Common; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; [TestFixture] public class BeatServiceTests : ServiceTestFixture diff --git a/tests/NovelSoftware.Tests/ListingTests.cs b/tests/Novelly.Api.Tests/ListingTests.cs similarity index 97% rename from tests/NovelSoftware.Tests/ListingTests.cs rename to tests/Novelly.Api.Tests/ListingTests.cs index b9d0a25..fbe1f9a 100644 --- a/tests/NovelSoftware.Tests/ListingTests.cs +++ b/tests/Novelly.Api.Tests/ListingTests.cs @@ -1,10 +1,12 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using NovelSoftware.Application.Agent; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain; +using Novelly.Api.Agent; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; /// /// Covers the list endpoints, which sort and aggregate in SQL rather than in memory. diff --git a/tests/NovelSoftware.Tests/NovelAgentServiceTests.cs b/tests/Novelly.Api.Tests/NovelAgentServiceTests.cs similarity index 98% rename from tests/NovelSoftware.Tests/NovelAgentServiceTests.cs rename to tests/Novelly.Api.Tests/NovelAgentServiceTests.cs index f045bd9..8a25673 100644 --- a/tests/NovelSoftware.Tests/NovelAgentServiceTests.cs +++ b/tests/Novelly.Api.Tests/NovelAgentServiceTests.cs @@ -1,10 +1,10 @@ using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using NovelSoftware.Application.Agent; -using NovelSoftware.Application.Dtos; +using Novelly.Api.Agent; +using Novelly.Api.Projects; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; [TestFixture] public class NovelAgentServiceTests : ServiceTestFixture diff --git a/tests/NovelSoftware.Tests/NovelSoftware.Tests.csproj b/tests/Novelly.Api.Tests/Novelly.Api.Tests.csproj similarity index 78% rename from tests/NovelSoftware.Tests/NovelSoftware.Tests.csproj rename to tests/Novelly.Api.Tests/Novelly.Api.Tests.csproj index 0f2b1ce..87f22d1 100644 --- a/tests/NovelSoftware.Tests/NovelSoftware.Tests.csproj +++ b/tests/Novelly.Api.Tests/Novelly.Api.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -24,8 +24,7 @@ - - + - \ No newline at end of file + diff --git a/tests/NovelSoftware.Tests/ProjectDataTests.cs b/tests/Novelly.Api.Tests/ProjectDataTests.cs similarity index 97% rename from tests/NovelSoftware.Tests/ProjectDataTests.cs rename to tests/Novelly.Api.Tests/ProjectDataTests.cs index 6bc39dc..2d99db6 100644 --- a/tests/NovelSoftware.Tests/ProjectDataTests.cs +++ b/tests/Novelly.Api.Tests/ProjectDataTests.cs @@ -1,9 +1,11 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application; -using NovelSoftware.Application.Dtos; -using NovelSoftware.Domain; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Common; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; [TestFixture] public class ProjectDataTests : ServiceTestFixture diff --git a/tests/NovelSoftware.Tests/ServiceTestFixture.cs b/tests/Novelly.Api.Tests/ServiceTestFixture.cs similarity index 88% rename from tests/NovelSoftware.Tests/ServiceTestFixture.cs rename to tests/Novelly.Api.Tests/ServiceTestFixture.cs index dbc3d82..4e1c4cc 100644 --- a/tests/NovelSoftware.Tests/ServiceTestFixture.cs +++ b/tests/Novelly.Api.Tests/ServiceTestFixture.cs @@ -1,6 +1,11 @@ -using NovelSoftware.Application.Services; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Projects; +using Novelly.Api.Scenes; +using Novelly.Api.Tags; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; /// /// Shared plumbing for the service tests: a fresh in-memory database and a matching set diff --git a/tests/NovelSoftware.Tests/TagServiceTests.cs b/tests/Novelly.Api.Tests/TagServiceTests.cs similarity index 97% rename from tests/NovelSoftware.Tests/TagServiceTests.cs rename to tests/Novelly.Api.Tests/TagServiceTests.cs index 0899991..277c119 100644 --- a/tests/NovelSoftware.Tests/TagServiceTests.cs +++ b/tests/Novelly.Api.Tests/TagServiceTests.cs @@ -1,7 +1,11 @@ using Microsoft.EntityFrameworkCore; -using NovelSoftware.Application.Dtos; +using Novelly.Api.Beats; +using Novelly.Api.Chapters; +using Novelly.Api.Characters; +using Novelly.Api.Projects; +using Novelly.Api.Tags; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; [TestFixture] public class TagServiceTests : ServiceTestFixture diff --git a/tests/NovelSoftware.Tests/TestDatabase.cs b/tests/Novelly.Api.Tests/TestDatabase.cs similarity index 94% rename from tests/NovelSoftware.Tests/TestDatabase.cs rename to tests/Novelly.Api.Tests/TestDatabase.cs index 562d84b..2ebd40a 100644 --- a/tests/NovelSoftware.Tests/TestDatabase.cs +++ b/tests/Novelly.Api.Tests/TestDatabase.cs @@ -1,8 +1,8 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using NovelSoftware.Infrastructure.Persistence; +using Novelly.Api.Data; -namespace NovelSoftware.Tests; +namespace Novelly.Api.Tests; /// /// A throwaway SQLite database held in memory. Using real SQLite rather than the