Add ChapterKind for front/back matter chapters

Chapters can now be marked FrontMatter/Body/BackMatter. Number stays
the manuscript sort key for every chapter; the author-facing display
number is now computed per-request as the chapter's ordinal among
Body chapters only, so a foreword or afterword no longer shifts the
numbering of the rest of the book. Surfaced through the API, agent
toolset, and import toolset.
This commit is contained in:
James Wampler
2026-08-19 17:54:20 -07:00
parent 71953220aa
commit ef5260a111
12 changed files with 1526 additions and 27 deletions
@@ -78,4 +78,62 @@ public class ChapterServiceTests : ServiceTestFixture
[Test]
public async Task Deleting_a_missing_chapter_returns_false_rather_than_throwing() =>
Assert.That(await Chapters.DeleteAsync(Guid.NewGuid()), Is.False);
[Test]
public async Task A_chapter_defaults_to_a_body_chapter()
{
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall"));
Assert.That(chapter!.Kind, Is.EqualTo(ChapterKind.Body));
}
[Test]
public async Task Front_matter_does_not_consume_a_chapter_number()
{
var foreword = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Foreword", Number: 1, Kind: ChapterKind.FrontMatter));
var first = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 2));
var second = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("The Harbour", Number: 3));
var displayNumbers = ChapterNumbering.DisplayNumbers(await Chapters.ListAsync(_novelId));
Assert.Multiple(() =>
{
Assert.That(displayNumbers.ContainsKey(foreword!.Id), Is.False);
Assert.That(displayNumbers[first!.Id], Is.EqualTo(1));
Assert.That(displayNumbers[second!.Id], Is.EqualTo(2));
});
}
[Test]
public async Task Back_matter_is_listed_last_but_carries_no_chapter_number()
{
var body = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 1));
var afterword = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Afterword", Number: 2, Kind: ChapterKind.BackMatter));
var afterwordDisplayNumber = await Chapters.DisplayNumberAsync(afterword!);
var bodyDisplayNumber = await Chapters.DisplayNumberAsync(body!);
Assert.Multiple(() =>
{
Assert.That(afterwordDisplayNumber, Is.Null);
Assert.That(bodyDisplayNumber, Is.EqualTo(1));
});
}
[Test]
public async Task Changing_a_chapter_to_front_matter_drops_it_from_the_chapter_count()
{
var chapter = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Prologue", Number: 1));
var other = await Chapters.CreateAsync(_novelId, new CreateChapterRequest("Landfall", Number: 2));
var updated = await Chapters.UpdateAsync(chapter!.Id, new UpdateChapterRequest(Kind: ChapterKind.FrontMatter));
var updatedDisplayNumber = await Chapters.DisplayNumberAsync(updated!);
var otherDisplayNumber = await Chapters.DisplayNumberAsync(other!);
Assert.Multiple(() =>
{
Assert.That(updatedDisplayNumber, Is.Null);
Assert.That(otherDisplayNumber, Is.EqualTo(1));
});
}
}