Remove Scenes, group beats by multiple characters; strip comments repo-wide

Drop the Scene entity/grouping in favor of chapters carrying prose directly
and beats belonging to many characters. Add markdown editor + character
multi-select components to the web client. Remove all XML doc and inline
comments across the touched C#/TS/CSS files in favor of self-documenting
names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP
server config, no secrets) and ignore .idea/.
This commit is contained in:
James Wampler
2026-08-11 21:05:13 -07:00
parent 1ce526019f
commit 23348327a9
57 changed files with 2600 additions and 1421 deletions
+11 -52
View File
@@ -1,9 +1,7 @@
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Scenes;
namespace Novelly.Api.Tests;
@@ -75,22 +73,20 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task A_beat_resolves_its_character_and_scene_names()
public async Task A_beat_can_carry_several_characters()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
"She burns the atlas",
CharacterId: ines.Id,
CharacterIds: [ines.Id, mara.Id],
WhatHappened: "The pages go up faster than she expected.",
WhatsNext: "Nothing to navigate by but memory.",
SceneId: scene.Id));
WhatsNext: "Nothing to navigate by but memory."));
Assert.Multiple(() =>
{
Assert.That(beat.Character!.Name, Is.EqualTo("Ines"));
Assert.That(beat.Scene!.Title, Is.EqualTo("The dock at dawn"));
Assert.That(beat.Characters.Select(c => c.Name), Is.EquivalentTo(new[] { "Ines", "Mara" }));
Assert.That(beat.WhatHappened, Does.Contain("faster than she expected"));
});
}
@@ -103,41 +99,10 @@ public class BeatServiceTests : ServiceTestFixture
Assert.That(
async () => await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("A beat", CharacterId: stranger.Id)),
_chapterId, new CreateBeatRequest("A beat", CharacterIds: [stranger.Id])),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
}
[Test]
public async Task A_beat_cannot_be_grouped_under_a_scene_from_another_chapter()
{
var elsewhere = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Elsewhere"));
var scene = await Scenes.CreateAsync(elsewhere.Id, new CreateSceneRequest("Another scene"));
Assert.That(
async () => await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("A beat", SceneId: scene.Id)),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same chapter"));
}
[Test]
public async Task Deleting_a_scene_leaves_its_beats_alone()
{
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
var beat = await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("She burns the atlas", SceneId: scene.Id));
await Scenes.DeleteAsync(scene.Id);
// The plan outlives a decision about prose — the beat is simply ungrouped.
var survivor = (await Beats.GetAsync(beat.Id))!;
Assert.Multiple(() =>
{
Assert.That(survivor.SceneId, Is.Null);
Assert.That(survivor.Title, Is.EqualTo("She burns the atlas"));
});
}
[Test]
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
{
@@ -164,23 +129,17 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task ClearCharacter_and_ClearScene_detach_the_reference_since_a_null_id_means_leave_it_alone()
public async Task An_empty_CharacterIds_list_clears_a_beats_characters_since_null_means_leave_it_alone()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
"She burns the atlas", CharacterId: ines.Id, SceneId: scene.Id));
"She burns the atlas", CharacterIds: [ines.Id]));
var untouched = (await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(Title: "She burns it")))!;
Assert.That(untouched.Character!.Name, Is.EqualTo("Ines"));
Assert.That(untouched.Characters.Select(c => c.Name), Is.EqualTo(new[] { "Ines" }));
var cleared = (await Beats.UpdateAsync(
beat.Id, new UpdateBeatRequest(ClearCharacter: true, ClearScene: true)))!;
var cleared = (await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(CharacterIds: [])))!;
Assert.Multiple(() =>
{
Assert.That(cleared.Character, Is.Null);
Assert.That(cleared.Scene, Is.Null);
});
Assert.That(cleared.Characters, Is.Empty);
}
}