Group character beats into arc-stage sections; reciprocal relationship types

Arc stages now group the beats that establish or pay off that stage of a
character's arc (many-to-many via ArcStageBeats), and carry a Result field
(renamed from Description) describing what the stage results in for the
character. Assigning a beat to a stage moves it out of any other stage of
the same character. New endpoint POST /api/arc-stages/{id}/beats, MCP tool
set_arc_stage_beats, and frontend grouping UI in CharacterArc/CharacterBeats.

Also records a relationship's reciprocal type so both characters' dossiers
show the correct direction (e.g. "sister" / "brother") instead of mirroring
the same label.
This commit is contained in:
James Wampler
2026-08-17 22:45:07 -07:00
parent eb1efcf9f8
commit 17facba3b9
27 changed files with 1871 additions and 74 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Novelly.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddArcStageResultAndBeats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Description",
table: "CharacterArcStages",
newName: "Result");
migrationBuilder.CreateTable(
name: "ArcStageBeats",
columns: table => new
{
ArcStagesId = table.Column<Guid>(type: "TEXT", nullable: false),
BeatsId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ArcStageBeats", x => new { x.ArcStagesId, x.BeatsId });
table.ForeignKey(
name: "FK_ArcStageBeats_Beats_BeatsId",
column: x => x.BeatsId,
principalTable: "Beats",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ArcStageBeats_CharacterArcStages_ArcStagesId",
column: x => x.ArcStagesId,
principalTable: "CharacterArcStages",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ArcStageBeats_BeatsId",
table: "ArcStageBeats",
column: "BeatsId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ArcStageBeats");
migrationBuilder.RenameColumn(
name: "Result",
table: "CharacterArcStages",
newName: "Description");
}
}
}
@@ -32,6 +32,21 @@ namespace Novelly.Api.Data.Migrations
b.ToTable("BeatCharacters", (string)null);
});
modelBuilder.Entity("BeatCharacterArcStage", b =>
{
b.Property<Guid>("ArcStagesId")
.HasColumnType("TEXT");
b.Property<Guid>("BeatsId")
.HasColumnType("TEXT");
b.HasKey("ArcStagesId", "BeatsId");
b.HasIndex("BeatsId");
b.ToTable("ArcStageBeats", (string)null);
});
modelBuilder.Entity("BeatTag", b =>
{
b.Property<Guid>("BeatsId")
@@ -398,7 +413,7 @@ namespace Novelly.Api.Data.Migrations
b.Property<long>("CreatedAt")
.HasColumnType("INTEGER");
b.Property<string>("Description")
b.Property<string>("Result")
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
@@ -856,6 +871,21 @@ namespace Novelly.Api.Data.Migrations
.IsRequired();
});
modelBuilder.Entity("BeatCharacterArcStage", b =>
{
b.HasOne("Novelly.Api.Characters.CharacterArcStage", null)
.WithMany()
.HasForeignKey("ArcStagesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Novelly.Api.Beats.Beat", null)
.WithMany()
.HasForeignKey("BeatsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BeatTag", b =>
{
b.HasOne("Novelly.Api.Beats.Beat", null)