Rename Project concept to Novel across the stack

Renames the domain concept from Project to Novel throughout the backend
(entities, DTOs, services, endpoints, ProjectAccessService/Permission,
ProjectId foreign keys), MCP server (tool names and routes), and the
React/Vite frontend (types, hooks, routes, components). Adds a new EF
Core migration (RenameProjectToNovel) using RenameTable/RenameColumn to
preserve existing data instead of dropping/recreating tables. Updates
CLAUDE.md's structure section to reference Novels/ instead of Projects/.
This commit is contained in:
James Wampler
2026-08-17 23:03:09 -07:00
parent 0ab4f568b5
commit 4313c8f206
95 changed files with 3192 additions and 1660 deletions
+17 -17
View File
@@ -3,7 +3,7 @@ using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
namespace Novelly.Api.Tests;
@@ -27,53 +27,53 @@ public class LoggingTests : ServiceTestFixture
}
[Test]
public async Task Creating_a_chapter_logs_the_project_and_title_at_information()
public async Task Creating_a_chapter_logs_the_novel_and_title_at_information()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
ChapterLogs.Entries.Clear();
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall"));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall"));
var info = ChapterLogs.Entries.Single(e => e.Level == LogLevel.Information);
Assert.Multiple(() =>
{
Assert.That(info.Message, Does.Contain("Landfall"));
Assert.That(info.Message, Does.Contain(project.Id.ToString()));
Assert.That(info.Message, Does.Contain(novel.Id.ToString()));
});
}
[Test]
public async Task Logged_values_never_include_a_chapter_summary_body()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
const string secretSummary = "A very specific plot twist nobody should see in a log line.";
ChapterLogs.Entries.Clear();
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall", Summary: secretSummary));
await Chapters.CreateAsync(novel.Id, new CreateChapterRequest("Landfall", Summary: secretSummary));
Assert.That(ChapterLogs.Entries.Select(e => e.Message), Has.None.Contain(secretSummary));
}
[Test]
public async Task Deleting_a_project_logs_information_before_the_lookup()
public async Task Deleting_a_novel_logs_information_before_the_lookup()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
ProjectLogs.Entries.Clear();
var novel = await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"));
NovelLogs.Entries.Clear();
await Projects.DeleteAsync(project.Id);
await Novels.DeleteAsync(novel.Id);
Assert.That(
ProjectLogs.Entries,
Has.Some.Matches<CapturedLogEntry>(e => e.Level == LogLevel.Information && e.Message.Contains(project.Id.ToString())));
NovelLogs.Entries,
Has.Some.Matches<CapturedLogEntry>(e => e.Level == LogLevel.Information && e.Message.Contains(novel.Id.ToString())));
}
[Test]
public async Task Rejecting_a_beat_with_a_foreign_character_logs_a_warning_not_an_error()
{
var projectA = await Projects.CreateAsync(new CreateProjectRequest("Project A"));
var projectB = await Projects.CreateAsync(new CreateProjectRequest("Project B"));
var chapter = await Chapters.CreateAsync(projectA.Id, new CreateChapterRequest("Landfall"));
var foreignCharacter = await Characters.CreateAsync(projectB.Id, new CreateCharacterRequest("Ines"));
var novelA = await Novels.CreateAsync(new CreateNovelRequest("Novel A"));
var novelB = await Novels.CreateAsync(new CreateNovelRequest("Novel B"));
var chapter = await Chapters.CreateAsync(novelA.Id, new CreateChapterRequest("Landfall"));
var foreignCharacter = await Characters.CreateAsync(novelB.Id, new CreateCharacterRequest("Ines"));
BeatLogs.Entries.Clear();
Assert.That(