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:
@@ -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;
|
||||
using Novelly.Api.Questions;
|
||||
using Novelly.Api.Tags;
|
||||
|
||||
@@ -23,7 +23,7 @@ public record AgentTool(
|
||||
Func<Guid, JsonElement, CancellationToken, Task<object?>> Handler);
|
||||
|
||||
public class NovelAgentToolset(
|
||||
ProjectService projects,
|
||||
NovelService novels,
|
||||
CharacterService characters,
|
||||
CharacterArcService arcs,
|
||||
ChapterService chapters,
|
||||
@@ -45,7 +45,7 @@ public class NovelAgentToolset(
|
||||
public IReadOnlyList<AgentToolDefinition> Definitions =>
|
||||
[.. Tools.Select(t => new AgentToolDefinition(t.Name, t.Description, t.InputSchema))];
|
||||
|
||||
public async Task<AgentToolResult> ExecuteAsync(string name, Guid projectId, JsonElement input, CancellationToken ct = default)
|
||||
public async Task<AgentToolResult> ExecuteAsync(string name, Guid novelId, JsonElement input, CancellationToken ct = default)
|
||||
{
|
||||
if (!ByName.TryGetValue(name, out var tool))
|
||||
{
|
||||
@@ -53,29 +53,29 @@ public class NovelAgentToolset(
|
||||
return new AgentToolResult($"No such tool: '{name}'.", true);
|
||||
}
|
||||
|
||||
logger.LogDebug("Running tool {Tool} for project {ProjectId}", name, projectId);
|
||||
logger.LogDebug("Running tool {Tool} for novel {NovelId}", name, novelId);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await tool.Handler(projectId, input, ct);
|
||||
var result = await tool.Handler(novelId, input, ct);
|
||||
|
||||
if (result is ToolNotFound notFound)
|
||||
{
|
||||
logger.LogWarning("Tool {Tool} for project {ProjectId} found no {Entity} {EntityId}", name, projectId, notFound.Entity, notFound.Id);
|
||||
logger.LogWarning("Tool {Tool} for novel {NovelId} found no {Entity} {EntityId}", name, novelId, notFound.Entity, notFound.Id);
|
||||
return new AgentToolResult(notFound.Message, true);
|
||||
}
|
||||
|
||||
logger.LogDebug("Tool {Tool} for project {ProjectId} succeeded", name, projectId);
|
||||
logger.LogDebug("Tool {Tool} for novel {NovelId} succeeded", name, novelId);
|
||||
return new AgentToolResult(JsonSerializer.Serialize(result, SerializerOptions), false);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Tool {Tool} for project {ProjectId} failed: invalid argument", name, projectId);
|
||||
logger.LogWarning(ex, "Tool {Tool} for novel {NovelId} failed: invalid argument", name, novelId);
|
||||
return new AgentToolResult(ex.Message, true);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Tool {Tool} for project {ProjectId} failed: invalid operation", name, projectId);
|
||||
logger.LogWarning(ex, "Tool {Tool} for novel {NovelId} failed: invalid operation", name, novelId);
|
||||
return new AgentToolResult(ex.Message, true);
|
||||
}
|
||||
}
|
||||
@@ -95,15 +95,15 @@ public class NovelAgentToolset(
|
||||
private IEnumerable<AgentTool> Build()
|
||||
{
|
||||
yield return new AgentTool(
|
||||
"get_project_brief",
|
||||
"Read the project's title, logline, synopsis, genre, notes and word-count target. "
|
||||
"get_novel_brief",
|
||||
"Read the novel's title, logline, synopsis, genre, notes and word-count target. "
|
||||
+ "Call this first in a conversation to ground yourself in what the book is.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (projectId, _, ct) => await OrNotFound(projects.GetAsync(projectId, ct), p => p.ToResponse(null), "Project", projectId));
|
||||
async (novelId, _, ct) => await OrNotFound(novels.GetAsync(novelId, ct), p => p.ToResponse(null), "Novel", novelId));
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_project_brief",
|
||||
"Revise the project's top-level fields. Only the fields you supply change; "
|
||||
"update_novel_brief",
|
||||
"Revise the novel's top-level fields. Only the fields you supply change; "
|
||||
+ "pass an empty string to clear a field.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("title", "New title.")
|
||||
@@ -114,27 +114,27 @@ public class NovelAgentToolset(
|
||||
.Str("notes", "Free-form notes on theme, tone, comparable titles.")
|
||||
.Int("target_word_count", "Target manuscript length in words.")
|
||||
.Build(),
|
||||
async (projectId, input, ct) => await OrNotFound(projects.UpdateAsync(projectId, new UpdateProjectRequest(
|
||||
async (novelId, input, ct) => await OrNotFound(novels.UpdateAsync(novelId, new UpdateNovelRequest(
|
||||
JsonInput.String(input, "title"),
|
||||
JsonInput.String(input, "author"),
|
||||
JsonInput.String(input, "genre"),
|
||||
JsonInput.String(input, "logline"),
|
||||
JsonInput.String(input, "synopsis"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Int(input, "target_word_count")), ct), p => p.ToResponse(null), "Project", projectId));
|
||||
JsonInput.Int(input, "target_word_count")), ct), p => p.ToResponse(null), "Novel", novelId));
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_characters",
|
||||
"List every character in the project with their full dossiers.",
|
||||
"List every character in the novel with their full dossiers.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (projectId, _, ct) => (await characters.ListAsync(projectId, ct)).Select(c => c.ToResponse()));
|
||||
async (novelId, _, ct) => (await characters.ListAsync(novelId, ct)).Select(c => c.ToResponse()));
|
||||
|
||||
yield return new AgentTool(
|
||||
"create_character",
|
||||
"Add a character dossier. Name is the only requirement — leave fields blank when "
|
||||
+ "the writer has not decided them yet rather than inventing detail.",
|
||||
CharacterSchema(includeName: true, nameRequired: true).Build(),
|
||||
async (projectId, input, ct) => await OrNotFound(characters.CreateAsync(projectId, new CreateCharacterRequest(
|
||||
async (novelId, input, ct) => await OrNotFound(characters.CreateAsync(novelId, new CreateCharacterRequest(
|
||||
JsonInput.RequiredString(input, "name"),
|
||||
JsonInput.Enum<CharacterRole>(input, "role") ?? CharacterRole.Supporting,
|
||||
JsonInput.Enum<CharacterImportance>(input, "importance") ?? CharacterImportance.Supporting,
|
||||
@@ -152,7 +152,7 @@ public class NovelAgentToolset(
|
||||
JsonInput.String(input, "voice"),
|
||||
JsonInput.String(input, "notes"),
|
||||
JsonInput.Strings(input, "tags"),
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Project", projectId));
|
||||
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Novel", novelId));
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_character",
|
||||
@@ -189,7 +189,7 @@ public class NovelAgentToolset(
|
||||
yield return new AgentTool(
|
||||
"link_character_identity",
|
||||
"Record that a character is really another character — e.g. one introduced under one name "
|
||||
+ "who is later revealed to be a character already in the project under another name. Both "
|
||||
+ "who is later revealed to be a character already in the novel under another name. Both "
|
||||
+ "keep their own dossier and beats; the canonical identity is whichever character you link to.",
|
||||
new JsonSchemaBuilder()
|
||||
.Str("character_id", "Id of the character being revealed as someone else.", required: true)
|
||||
@@ -348,10 +348,10 @@ public class NovelAgentToolset(
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_tags",
|
||||
"List the project's tags with how many characters, chapters and beats carry each. "
|
||||
"List the novel's tags with how many characters, chapters and beats carry each. "
|
||||
+ "Read this before inventing a new tag so you reuse the writer's vocabulary.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (projectId, _, ct) => await tags.ListAsync(projectId, ct));
|
||||
async (novelId, _, ct) => await tags.ListAsync(novelId, ct));
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_tag_references",
|
||||
@@ -368,9 +368,9 @@ public class NovelAgentToolset(
|
||||
|
||||
yield return new AgentTool(
|
||||
"list_chapters",
|
||||
"List the project's chapters in manuscript order with beat and word counts.",
|
||||
"List the novel's chapters in manuscript order with beat and word counts.",
|
||||
new JsonSchemaBuilder().Build(),
|
||||
async (projectId, _, ct) => (await chapters.ListAsync(projectId, ct)).Select(c => c.ToSummaryResponse()));
|
||||
async (novelId, _, ct) => (await chapters.ListAsync(novelId, ct)).Select(c => c.ToSummaryResponse()));
|
||||
|
||||
yield return new AgentTool(
|
||||
"get_chapter",
|
||||
@@ -398,7 +398,7 @@ public class NovelAgentToolset(
|
||||
.Str("prose", "The chapter's drafted text, in markdown, if you are writing it now.")
|
||||
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
|
||||
.Build(),
|
||||
async (projectId, input, ct) => await OrNotFound(chapters.CreateAsync(projectId, new CreateChapterRequest(
|
||||
async (novelId, input, ct) => await OrNotFound(chapters.CreateAsync(novelId, new CreateChapterRequest(
|
||||
JsonInput.RequiredString(input, "title"),
|
||||
JsonInput.Int(input, "number"),
|
||||
JsonInput.String(input, "summary"),
|
||||
@@ -407,7 +407,7 @@ public class NovelAgentToolset(
|
||||
JsonInput.Enum<DraftStatus>(input, "status") ?? DraftStatus.Planned,
|
||||
JsonInput.Int(input, "target_word_count"),
|
||||
JsonInput.String(input, "prose"),
|
||||
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Project", projectId));
|
||||
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Novel", novelId));
|
||||
|
||||
yield return new AgentTool(
|
||||
"update_chapter",
|
||||
@@ -548,8 +548,8 @@ public class NovelAgentToolset(
|
||||
.Str("character_id", "Narrow to questions about one character.")
|
||||
.Bool("include_resolved", "Include questions already settled. Defaults to false.")
|
||||
.Build(),
|
||||
async (projectId, input, ct) => (await questions.ListAsync(
|
||||
projectId,
|
||||
async (novelId, input, ct) => (await questions.ListAsync(
|
||||
novelId,
|
||||
JsonInput.Guid(input, "chapter_id"),
|
||||
JsonInput.Guid(input, "character_id"),
|
||||
JsonInput.Bool(input, "include_resolved") ?? false,
|
||||
@@ -566,13 +566,13 @@ public class NovelAgentToolset(
|
||||
.Str("chapter_id", "The chapter outline this is about, if any.")
|
||||
.Str("character_id", "The character this is about, if any.")
|
||||
.Build(),
|
||||
async (projectId, input, ct) => await OrNotFound(questions.CreateAsync(
|
||||
projectId,
|
||||
async (novelId, input, ct) => await OrNotFound(questions.CreateAsync(
|
||||
novelId,
|
||||
new CreateOpenQuestionRequest(
|
||||
JsonInput.RequiredString(input, "question"),
|
||||
JsonInput.String(input, "detail"),
|
||||
JsonInput.Guid(input, "chapter_id"),
|
||||
JsonInput.Guid(input, "character_id")), ct), q => q.ToResponse(), "Project", projectId));
|
||||
JsonInput.Guid(input, "character_id")), ct), q => q.ToResponse(), "Novel", novelId));
|
||||
|
||||
yield return new AgentTool(
|
||||
"resolve_open_question",
|
||||
|
||||
Reference in New Issue
Block a user