Update README for locations, Novel rename, and scene removal

Docs had drifted from the code across several prior renames; refreshes the
data model, API surface table, and tool counts to match, and documents the
new Locations feature and chapter character summary alongside the actual
current commit.
This commit is contained in:
James Wampler
2026-08-18 11:40:39 -07:00
parent c620ddd626
commit 56e64c6f06
+40 -30
View File
@@ -21,7 +21,7 @@ same edit.
| `Novelly.Web` | React 19, TypeScript, Vite, TanStack Query, Tailwind v4 | | `Novelly.Web` | React 19, TypeScript, Vite, TanStack Query, Tailwind v4 |
The back end is one project organised by feature, not by layer. Each feature folder — The back end is one project organised by feature, not by layer. Each feature folder —
`Projects/`, `Characters/`, `Chapters/`, `Beats/`, `Scenes/`, `Tags/`, `Agent/` — holds its `Novels/`, `Characters/`, `Chapters/`, `Beats/`, `Tags/`, `Locations/`, `Agent/` — holds its
entity, DTOs, service and endpoints together, so adding a capability means touching one entity, DTOs, service and endpoints together, so adding a capability means touching one
folder rather than four. `Common/` holds what genuinely crosses features and `Data/` holds folder rather than four. `Common/` holds what genuinely crosses features and `Data/` holds
the `DbContext` and migrations. the `DbContext` and migrations.
@@ -65,7 +65,7 @@ and everything else keeps working.
### Tests ### Tests
```bash ```bash
dotnet test # 73 tests dotnet test # 174 tests
cd src/Novelly.Web && npm run build # typecheck + bundle cd src/Novelly.Web && npm run build # typecheck + bundle
``` ```
@@ -98,13 +98,13 @@ out of `appsettings.json` and use user-secrets or the environment.
## The data model ## The data model
``` ```
Project ──┬── Character ──┬── CharacterRelationship Novel ──┬── Character ──┬── CharacterRelationship
│ └── CharacterArcStage (the arc: flat, ordered) │ └── CharacterArcStage (the arc: flat, ordered)
├── Chapter ──── Beat (the outline: flat, ordered) ├── Chapter ──── Beat (the outline: flat, ordered)
│ └── Scene (the prose) ├── Tag (applied to characters, chapters and beats)
├── Tag (applied to characters, chapters and beats) ├── Location (applied to chapters)
├── OpenQuestion (attached to a chapter and/or a character) ├── OpenQuestion (attached to a chapter and/or a character)
└── AgentConversation ── AgentMessage └── AgentConversation ── AgentMessage
``` ```
**A chapter outline is a paragraph plus a table.** The paragraph is the chapter's **A chapter outline is a paragraph plus a table.** The paragraph is the chapter's
@@ -113,18 +113,20 @@ Project ──┬── Character ──┬── CharacterRelationship
| Column | What goes in it | | Column | What goes in it |
|---|---| |---|---|
| Beat | A three-to-five word handle — "she burns the atlas", not a sentence | | Beat | A three-to-five word handle — "she burns the atlas", not a sentence |
| Character | Whose beat it is. Optional; not every beat belongs to one person | | Characters | Who's in the beat. Optional; not every beat belongs to anyone, and a beat can name more than one |
| What happened | The event itself | | What happened | The event itself |
| What's next | What it sets in motion — the hook into the following beat | | What's next | What it sets in motion — the hook into the following beat |
| Scene | Optional grouping: which scene will carry this beat's prose |
Beats are flat and ordered by `SortOrder` within their chapter. There is no nesting and Beats are flat and ordered by `SortOrder` within their chapter. There is no nesting and
no tree — reordering is one call that takes the beat ids in the order wanted. no tree — reordering is one call that takes the beat ids in the order wanted. The outline
tab also rolls up the distinct set of characters across a chapter's beats under the beat
and word counts, each linking to that character's page — a quick cast list without
opening every beat.
**Beats plan; scenes carry prose.** The two layers are deliberately separate: an outline **Beats plan; the chapter's `Prose` carries the draft.** A chapter has one prose field,
is for working out what happens, and a scene is where you write it. A beat's `SceneId` is written and redrafted in place; `WordCount` is recomputed from it whenever it changes.
the optional link between them, and it is nullable in both directions — deleting a scene There is no separate scene entity — the outline (beats) and the draft (prose) are the
ungroups its beats rather than deleting the plan. two views of the same chapter.
**Main characters carry the book; supporting characters hold it up.** A character's **Main characters carry the book; supporting characters hold it up.** A character's
`Importance` (`Main` or `Supporting`) is separate from their `Role` — role is the part they `Importance` (`Main` or `Supporting`) is separate from their `Role` — role is the part they
@@ -149,19 +151,26 @@ append the decision to the notes of whatever it was attached to — so a settled
up where you re-read it rather than in a list you have stopped looking at. Resolved up where you re-read it rather than in a list you have stopped looking at. Resolved
questions drop off the list unless you ask for them. questions drop off the list unless you ask for them.
**Tags cross-reference the book.** A tag is scoped to one project, unique by name **Tags cross-reference the book.** A tag is scoped to one novel, unique by name
(case-insensitively), and can be attached to any character, chapter or beat. Applying an (case-insensitively), and can be attached to any character, chapter or beat. Applying an
unknown tag by name creates it, so tagging is one action rather than two. `GET unknown tag by name creates it, so tagging is one action rather than two. `GET
/api/tags/{id}/references` returns everything carrying a tag, which is how you trace a /api/tags/{id}/references` returns everything carrying a tag, which is how you trace a
motif or a thread across all three kinds at once. motif or a thread across all three kinds at once.
**A chapter can have several locations.** Locations replaced the old single free-text
`Setting` field: a chapter now carries a multi-select list of locations (where and when
it takes place), each a novel-scoped, name-deduped entity — applying an unknown location
by name creates it, same as tags. A "Locations" tab on the novel lists every location
with its chapter count, and `GET /api/locations/{id}/references` lists every chapter set
there.
## The embedded agent ## The embedded agent
`NovelAgentService` runs the tool-use loop: it calls the Messages API, executes any tools `NovelAgentService` runs the tool-use loop: it calls the Messages API, executes any tools
Claude asks for, feeds every result back in a single user turn, and repeats until Claude Claude asks for, feeds every result back in a single user turn, and repeats until Claude
stops asking. It has 29 tools covering the brief, characters and their arcs, chapter outlines stops asking. It has 33 tools covering the brief, characters and their arcs, chapter
(beats), scenes, tags and open questions — all of them going through the same application outlines (beats), tags, locations and open questions — all of them going through the same
services the REST API uses. application services the REST API uses.
A few deliberate choices worth knowing about: A few deliberate choices worth knowing about:
@@ -179,7 +188,7 @@ A few deliberate choices worth knowing about:
## The MCP server ## The MCP server
A stdio MCP server exposing 38 tools over the same REST API. It holds no domain logic of A stdio MCP server exposing 45 tools over the same REST API. It holds no domain logic of
its own — it is a second front end, not a second implementation. its own — it is a second front end, not a second implementation.
Build it, then point your MCP client at the produced binary: Build it, then point your MCP client at the produced binary:
@@ -230,19 +239,20 @@ running and `.mcp.json` is set up.
| Resource | Routes | | Resource | Routes |
|---|---| |---|---|
| Projects | `GET\|POST /api/projects`, `GET\|PATCH\|DELETE /api/projects/{id}` | | Novels | `GET\|POST /api/novels`, `GET\|PATCH\|DELETE /api/novels/{id}` |
| Characters | `GET\|POST /api/projects/{id}/characters`, `GET\|PATCH\|DELETE /api/characters/{id}`, `POST /api/characters/{id}/relationships` | | Characters | `GET\|POST /api/novels/{id}/characters`, `GET\|PATCH\|DELETE /api/characters/{id}`, `POST /api/characters/{id}/relationships` |
| Chapters | `GET\|POST /api/projects/{id}/chapters`, `GET\|PATCH\|DELETE /api/chapters/{id}` | | Chapters | `GET\|POST /api/novels/{id}/chapters`, `GET\|PATCH\|DELETE /api/chapters/{id}` |
| Beats | `GET\|POST /api/chapters/{id}/beats`, `POST /api/chapters/{id}/beats/reorder`, `GET\|PATCH\|DELETE /api/beats/{id}` | | Beats | `GET\|POST /api/chapters/{id}/beats`, `POST /api/chapters/{id}/beats/reorder`, `GET\|PATCH\|DELETE /api/beats/{id}` |
| Scenes | `GET\|POST /api/chapters/{id}/scenes`, `GET\|PATCH\|DELETE /api/scenes/{id}` | | Tags | `GET\|POST /api/novels/{id}/tags`, `GET /api/tags/{id}/references`, `PATCH\|DELETE /api/tags/{id}` |
| Tags | `GET\|POST /api/projects/{id}/tags`, `GET /api/tags/{id}/references`, `PATCH\|DELETE /api/tags/{id}` | | Locations | `GET\|POST /api/novels/{id}/locations`, `GET /api/locations/{id}/references`, `PATCH\|DELETE /api/locations/{id}` |
| Arcs | `GET\|POST /api/characters/{id}/arc`, `POST /api/characters/{id}/arc/reorder`, `GET\|PATCH\|DELETE /api/arc-stages/{id}` | | Arcs | `GET\|POST /api/characters/{id}/arc`, `POST /api/characters/{id}/arc/reorder`, `GET\|PATCH\|DELETE /api/arc-stages/{id}` |
| Questions | `GET\|POST /api/projects/{id}/questions`, `GET\|PATCH\|DELETE /api/questions/{id}`, `POST /api/questions/{id}/resolve`, `POST /api/questions/{id}/reopen` | | Questions | `GET\|POST /api/novels/{id}/questions`, `GET\|PATCH\|DELETE /api/questions/{id}`, `POST /api/questions/{id}/resolve`, `POST /api/questions/{id}/reopen` |
| Agent | `GET /api/projects/{id}/agent/conversations`, `POST /api/projects/{id}/agent/messages`, `GET\|DELETE /api/conversations/{id}` | | Agent | `GET /api/novels/{id}/agent/conversations`, `POST /api/novels/{id}/agent/messages`, `GET\|DELETE /api/conversations/{id}` |
`PATCH` bodies are partial: an omitted field is left alone, an empty string clears it. A `PATCH` bodies are partial: an omitted field is left alone, an empty string clears it. A
`tags` array replaces that item's tags outright and creates any names the project has not `tags` array replaces that item's tags outright and creates any names the novel has not
seen; omitting it leaves tags untouched. seen; the same is true of a chapter's `locations` array. Omitting either leaves it
untouched.
Enums travel as names (`"Protagonist"`, `"Drafted"`), never ordinals. In development the Enums travel as names (`"Protagonist"`, `"Drafted"`), never ordinals. In development the
OpenAPI document is at `/openapi/v1.json`. OpenAPI document is at `/openapi/v1.json`.