Builds out the vertical slice for planning and writing a novel. Three front ends — the React UI, an embedded Claude agent, and an MCP stdio server — all go through one REST API, so an edit made from Claude Code and one made in the browser are the same edit. Layout: Domain entities and enums, no dependencies Application services, DTOs, the agent tool-use loop and its 15 tools Infrastructure EF Core 10 + SQLite, Anthropic SDK client Api ASP.NET Core 10 minimal APIs, OpenAPI, ProblemDetails Mcp MCP stdio server, 21 tools over the same REST API Web React 19 + Vite + TanStack Query + Tailwind v4 Data model is Project > Characters / OutlineNodes / Chapters > Scenes, plus agent conversations. The outline is a self-nesting tree so acts, sequences and beats can be arranged however the book wants; scenes carry goal/conflict/outcome because that is what the agent drafts prose from. Notes on a few choices: - Conversation history replays to the model as text only. The agent re-reads current state through its tools rather than trusting a record of edits that may since have changed in the UI. - The user's turn is persisted before the tool loop runs, so a question is recorded even when the model call fails. Turn order uses an explicit sequence column; timestamps tie when a turn completes inside one tick. - Tool failures return is_error results rather than throwing, so the model can read the message and correct itself. MCP tools do the same via CallToolResult, which keeps the API's own message instead of a generic SDK error. - The Anthropic client is constructed lazily. It is injected into the agent service, which also serves read-only endpoints, and those should keep working on an install with no key. Sending without one returns 503, not 400. - DateTimeOffset is stored as UTC ticks. SQLite refuses to ORDER BY the default text form, which every "recently updated first" listing depends on. Tests run against real in-memory SQLite rather than the EF in-memory provider so they exercise the cascade deletes and query translation that actually ship. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
530 lines
18 KiB
C#
530 lines
18 KiB
C#
// <auto-generated />
|
|
using System;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
using NovelSoftware.Infrastructure.Persistence;
|
|
|
|
#nullable disable
|
|
|
|
namespace NovelSoftware.Infrastructure.Persistence.Migrations
|
|
{
|
|
[DbContext(typeof(NovelDbContext))]
|
|
partial class NovelDbContextModelSnapshot : ModelSnapshot
|
|
{
|
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
|
{
|
|
#pragma warning disable 612, 618
|
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<Guid>("ProjectId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Title")
|
|
.IsRequired()
|
|
.HasMaxLength(200)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("UpdatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("ProjectId");
|
|
|
|
b.ToTable("Conversations");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Content")
|
|
.IsRequired()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("ConversationId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Role")
|
|
.IsRequired()
|
|
.HasMaxLength(16)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<int>("Sequence")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("ToolCallsJson")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("ConversationId", "Sequence")
|
|
.IsUnique();
|
|
|
|
b.ToTable("AgentMessages");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Notes")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<int>("Number")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<Guid?>("PovCharacterId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("ProjectId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Setting")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Status")
|
|
.IsRequired()
|
|
.HasMaxLength(32)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Summary")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<int?>("TargetWordCount")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Title")
|
|
.IsRequired()
|
|
.HasMaxLength(300)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("UpdatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("PovCharacterId");
|
|
|
|
b.HasIndex("ProjectId", "Number");
|
|
|
|
b.ToTable("Chapters");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Age")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Appearance")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("ArcSummary")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Backstory")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("ExternalConflict")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("InternalConflict")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Name")
|
|
.IsRequired()
|
|
.HasMaxLength(200)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Need")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Notes")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Occupation")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Personality")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("ProjectId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Pronouns")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Role")
|
|
.IsRequired()
|
|
.HasMaxLength(32)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("UpdatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Voice")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Want")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("ProjectId");
|
|
|
|
b.ToTable("Characters");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("CharacterId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Description")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("RelatedCharacterId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("RelationshipType")
|
|
.IsRequired()
|
|
.HasMaxLength(120)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("CharacterId");
|
|
|
|
b.HasIndex("RelatedCharacterId");
|
|
|
|
b.ToTable("CharacterRelationships");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.OutlineNode", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid?>("ChapterId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("NodeType")
|
|
.IsRequired()
|
|
.HasMaxLength(32)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid?>("ParentId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("ProjectId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<int>("SortOrder")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Summary")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Title")
|
|
.IsRequired()
|
|
.HasMaxLength(300)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("UpdatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("ChapterId");
|
|
|
|
b.HasIndex("ParentId");
|
|
|
|
b.HasIndex("ProjectId", "ParentId", "SortOrder");
|
|
|
|
b.ToTable("OutlineNodes");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Author")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Genre")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Logline")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Notes")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Synopsis")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<int?>("TargetWordCount")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Title")
|
|
.IsRequired()
|
|
.HasMaxLength(300)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("UpdatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.ToTable("Projects");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b =>
|
|
{
|
|
b.Property<Guid>("Id")
|
|
.ValueGeneratedOnAdd()
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid>("ChapterId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Conflict")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("CreatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Goal")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Location")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Outcome")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<Guid?>("PovCharacterId")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Prose")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<int>("SortOrder")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<string>("Status")
|
|
.IsRequired()
|
|
.HasMaxLength(32)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Summary")
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<string>("Title")
|
|
.IsRequired()
|
|
.HasMaxLength(300)
|
|
.HasColumnType("TEXT");
|
|
|
|
b.Property<long>("UpdatedAt")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.Property<int>("WordCount")
|
|
.HasColumnType("INTEGER");
|
|
|
|
b.HasKey("Id");
|
|
|
|
b.HasIndex("PovCharacterId");
|
|
|
|
b.HasIndex("ChapterId", "SortOrder");
|
|
|
|
b.ToTable("Scenes");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.Project", "Project")
|
|
.WithMany("Conversations")
|
|
.HasForeignKey("ProjectId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.Navigation("Project");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentMessage", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.AgentConversation", "Conversation")
|
|
.WithMany("Messages")
|
|
.HasForeignKey("ConversationId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.Navigation("Conversation");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter")
|
|
.WithMany()
|
|
.HasForeignKey("PovCharacterId")
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
|
|
b.HasOne("NovelSoftware.Domain.Entities.Project", "Project")
|
|
.WithMany("Chapters")
|
|
.HasForeignKey("ProjectId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.Navigation("PovCharacter");
|
|
|
|
b.Navigation("Project");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.Project", "Project")
|
|
.WithMany("Characters")
|
|
.HasForeignKey("ProjectId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.Navigation("Project");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.CharacterRelationship", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.Character", "Character")
|
|
.WithMany("Relationships")
|
|
.HasForeignKey("CharacterId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.HasOne("NovelSoftware.Domain.Entities.Character", "RelatedCharacter")
|
|
.WithMany()
|
|
.HasForeignKey("RelatedCharacterId")
|
|
.OnDelete(DeleteBehavior.Restrict)
|
|
.IsRequired();
|
|
|
|
b.Navigation("Character");
|
|
|
|
b.Navigation("RelatedCharacter");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.OutlineNode", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter")
|
|
.WithMany()
|
|
.HasForeignKey("ChapterId")
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
|
|
b.HasOne("NovelSoftware.Domain.Entities.OutlineNode", "Parent")
|
|
.WithMany("Children")
|
|
.HasForeignKey("ParentId")
|
|
.OnDelete(DeleteBehavior.Restrict);
|
|
|
|
b.HasOne("NovelSoftware.Domain.Entities.Project", "Project")
|
|
.WithMany("OutlineNodes")
|
|
.HasForeignKey("ProjectId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.Navigation("Chapter");
|
|
|
|
b.Navigation("Parent");
|
|
|
|
b.Navigation("Project");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Scene", b =>
|
|
{
|
|
b.HasOne("NovelSoftware.Domain.Entities.Chapter", "Chapter")
|
|
.WithMany("Scenes")
|
|
.HasForeignKey("ChapterId")
|
|
.OnDelete(DeleteBehavior.Cascade)
|
|
.IsRequired();
|
|
|
|
b.HasOne("NovelSoftware.Domain.Entities.Character", "PovCharacter")
|
|
.WithMany()
|
|
.HasForeignKey("PovCharacterId")
|
|
.OnDelete(DeleteBehavior.SetNull);
|
|
|
|
b.Navigation("Chapter");
|
|
|
|
b.Navigation("PovCharacter");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.AgentConversation", b =>
|
|
{
|
|
b.Navigation("Messages");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Chapter", b =>
|
|
{
|
|
b.Navigation("Scenes");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Character", b =>
|
|
{
|
|
b.Navigation("Relationships");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.OutlineNode", b =>
|
|
{
|
|
b.Navigation("Children");
|
|
});
|
|
|
|
modelBuilder.Entity("NovelSoftware.Domain.Entities.Project", b =>
|
|
{
|
|
b.Navigation("Chapters");
|
|
|
|
b.Navigation("Characters");
|
|
|
|
b.Navigation("Conversations");
|
|
|
|
b.Navigation("OutlineNodes");
|
|
});
|
|
#pragma warning restore 612, 618
|
|
}
|
|
}
|
|
}
|