Reorganise by feature, rename to Novelly, add Aspire and a pre-push hook

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
This commit is contained in:
James Wampler
2026-08-06 12:11:20 -07:00
co-authored by Claude Opus 5
parent 30e0c6926e
commit 725758ccd9
120 changed files with 811 additions and 421 deletions
@@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\NovelSoftware.Infrastructure\NovelSoftware.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.OpenApi" Version="2.11.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\NovelSoftware.Domain\NovelSoftware.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.10" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
-31
View File
@@ -1,31 +0,0 @@
namespace NovelSoftware.Domain;
/// <summary>The role a character plays in the story.</summary>
public enum CharacterRole
{
Protagonist,
Antagonist,
Deuteragonist,
Supporting,
Minor,
Mentor,
LoveInterest,
Foil
}
/// <summary>How far along a chapter or scene is in the drafting pipeline.</summary>
public enum DraftStatus
{
Planned,
Outlined,
Drafted,
Revised,
Final
}
/// <summary>Who produced a message in an agent conversation.</summary>
public enum AgentRole
{
User,
Assistant
}
@@ -1,10 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
@@ -1,6 +1,6 @@
using System.Text.Json;
namespace NovelSoftware.Application.Agent;
namespace Novelly.Api.Agent;
/// <summary>A tool the model may call, described in the shape the Messages API expects.</summary>
public record AgentToolDefinition(string Name, string Description, JsonElement InputSchema);
@@ -1,4 +1,6 @@
namespace NovelSoftware.Domain.Entities;
using Novelly.Api.Projects;
namespace Novelly.Api.Agent;
/// <summary>A chat thread between the writer and the embedded agent, scoped to one project.</summary>
public class AgentConversation
@@ -1,6 +1,4 @@
using NovelSoftware.Domain;
namespace NovelSoftware.Application.Dtos;
namespace Novelly.Api.Agent;
public record ConversationSummaryDto(
Guid Id,
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Agent;
using NovelSoftware.Application.Dtos;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Agent;
public static class AgentEndpoints
{
+8
View File
@@ -0,0 +1,8 @@
namespace Novelly.Api.Agent;
/// <summary>Who produced a message in an agent conversation.</summary>
public enum AgentRole
{
User,
Assistant
}
@@ -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;
/// <summary>
/// Talks to the Anthropic Messages API. Translates between the application's
@@ -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;
/// <summary>
/// Small builder for the JSON Schema objects tool definitions need. Hand-writing these
@@ -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;
/// <summary>
/// The embedded writing agent. Runs the tool-use loop against the model, persists the
@@ -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;
/// <summary>The outcome of running a tool: what to hand back to the model, and whether it failed.</summary>
public record AgentToolResult(string Content, bool IsError);
@@ -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;
/// <summary>
/// One row of a chapter's outline: a short label, who it belongs to, what happened, and
@@ -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,
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Beats;
public static class BeatEndpoints
{
@@ -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;
/// <summary>
/// Beats are a chapter's outline: a flat, ordered table rather than a tree. Everything
@@ -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;
/// <summary>A chapter: an ordered container of scenes plus its own planning fields.</summary>
public class Chapter
@@ -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,
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Chapters;
public static class ChapterEndpoints
{
@@ -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)
{
@@ -1,4 +1,7 @@
namespace NovelSoftware.Domain.Entities;
using Novelly.Api.Projects;
using Novelly.Api.Tags;
namespace Novelly.Api.Characters;
/// <summary>
/// A character dossier. Every field beyond <see cref="Name"/> is optional so a writer can
@@ -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,
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Characters;
public static class CharacterEndpoints
{
@@ -0,0 +1,14 @@
namespace Novelly.Api.Characters;
/// <summary>The role a character plays in the story.</summary>
public enum CharacterRole
{
Protagonist,
Antagonist,
Deuteragonist,
Supporting,
Minor,
Mentor,
LoveInterest,
Foil
}
@@ -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)
{
@@ -1,4 +1,4 @@
namespace NovelSoftware.Application;
namespace Novelly.Api.Common;
/// <summary>
/// Thrown when the agent is asked to run but has no model credentials. This is a
+11
View File
@@ -0,0 +1,11 @@
namespace Novelly.Api.Common;
/// <summary>How far along a chapter or scene is in the drafting pipeline.</summary>
public enum DraftStatus
{
Planned,
Outlined,
Drafted,
Revised,
Final
}
@@ -1,4 +1,4 @@
namespace NovelSoftware.Application;
namespace Novelly.Api.Common;
/// <summary>
/// Thrown when a service is asked for an entity that does not exist. The API translates
@@ -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
/// <summary>
/// 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.
/// </summary>
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";
+15
View File
@@ -0,0 +1,15 @@
namespace Novelly.Api.Common;
/// <summary>
/// Patch semantics shared by every update endpoint: a null value leaves the field
/// untouched, an empty string clears it.
/// </summary>
internal static class Patch
{
public static string? Apply(string? current, string? incoming) => incoming switch
{
null => current,
"" => null,
_ => incoming
};
}
@@ -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;
/// <summary>
/// The persistence surface the application services depend on. Infrastructure supplies
@@ -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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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");
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NovelSoftware.Infrastructure.Persistence.Migrations
namespace Novelly.Api.Migrations
{
/// <inheritdoc />
public partial class InitialSchema : Migration
@@ -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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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");
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NovelSoftware.Infrastructure.Persistence.Migrations
namespace Novelly.Api.Migrations
{
/// <inheritdoc />
public partial class ReplaceOutlineWithBeatsAndTags : Migration
@@ -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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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<Guid>("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");
@@ -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;
/// <summary>
/// Stores a <see cref="DateTimeOffset"/> as UTC ticks. SQLite has no native type for it
@@ -1,25 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\NovelSoftware.Application\NovelSoftware.Application.csproj" />
<ProjectReference Include="..\Novelly.ServiceDefaults\Novelly.ServiceDefaults.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Anthropic" Version="12.39.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
<PackageReference Include="Microsoft.OpenApi" Version="2.11.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
@@ -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()
@@ -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;
/// <summary>A single novel and everything that belongs to it.</summary>
public class Project
@@ -1,6 +1,4 @@
using NovelSoftware.Domain.Entities;
namespace NovelSoftware.Application.Dtos;
namespace Novelly.Api.Projects;
public record ProjectSummaryDto(
Guid Id,
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Projects;
public static class ProjectEndpoints
{
@@ -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);
}
/// <summary>
/// Patch semantics shared by every update endpoint: a null value leaves the field
/// untouched, an empty string clears it.
/// </summary>
internal static class Patch
{
public static string? Apply(string? current, string? incoming) => incoming switch
{
null => current,
"" => null,
_ => incoming
};
}
@@ -1,4 +1,8 @@
namespace NovelSoftware.Domain.Entities;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
namespace Novelly.Api.Scenes;
/// <summary>
/// A scene inside a chapter. The goal/conflict/outcome trio is the unit the agent
@@ -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,
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Scenes;
public static class SceneEndpoints
{
@@ -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)
{
@@ -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;
/// <summary>
/// A free-form label scoped to one project. Tags are the cross-reference mechanism:
@@ -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);
@@ -1,7 +1,4 @@
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Api.Endpoints;
namespace Novelly.Api.Tags;
public static class TagEndpoints
{
@@ -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)
{
@@ -2,7 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"NovelSoftware": "Debug",
"Novelly": "Debug",
"Microsoft.AspNetCore": "Warning"
}
}
+15
View File
@@ -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<Projects.Novelly_Api>("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();
@@ -0,0 +1,21 @@
<Project Sdk="Aspire.AppHost.Sdk/13.4.6">
<ItemGroup>
<ProjectReference Include="..\Novelly.Api\Novelly.Api.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.JavaScript" Version="13.4.6" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<UserSecretsId>4f9d2a13-6c85-4b0e-9a7c-2d3f18b6e0c4</UserSecretsId>
</PropertyGroup>
</Project>
+29
View File
@@ -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"
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Dashboard": {
"Frontend": {
"AuthMode": "Unsecured"
},
"Otlp": {
"AuthMode": "Unsecured"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"appHost": {
"path": "Novelly.AppHost.csproj"
}
}
@@ -3,10 +3,10 @@ using System.Net.Http.Json;
using System.Text.Json;
using ModelContextProtocol.Protocol;
namespace NovelSoftware.Mcp;
namespace Novelly.Mcp;
/// <summary>
/// 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.
/// </summary>
@@ -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);
@@ -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<NovelApiClient>(client =>
{
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+127
View File
@@ -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<TBuilder>(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<ServiceDiscoveryOptions>(options =>
// {
// options.AllowedSchemes = ["https"];
// });
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(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<TBuilder>(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<TBuilder>(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;
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.8.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.8.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
</ItemGroup>
</Project>
@@ -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",
@@ -1,5 +1,5 @@
{
"name": "novelsoftware-web",
"name": "novelly-web",
"private": true,
"version": "0.0.0",
"type": "module",

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -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'