Reorganise by feature, rename to Novelly, add Aspire and a pre-push hook

The layered split into Domain/Application/Infrastructure/Api was forcing
organisation by layer: adding one capability meant touching four projects and
four folders that each held a slice of it. Those four projects are now one
feature-organised Novelly.Api, where each folder — Projects, Characters,
Chapters, Beats, Scenes, Tags, Agent — holds its entity, DTOs, service and
endpoints together. Common/ holds what genuinely crosses features (the patch
semantics, the two exception types, DraftStatus) and Data/ holds the DbContext
and migrations.

Six .NET projects become five: the three layer projects are gone, and
Novelly.AppHost and Novelly.ServiceDefaults are new.

- Namespaces move from NovelSoftware.* to Novelly.*, including the entity type
  names recorded in the EF model snapshots. The migration ids are untouched, so
  an existing novel.db still migrates cleanly — verified against a fresh file.
- Aspire orchestration mirrors the mic-check setup: the AppHost starts the API
  on :5080 and the Vite dev server on :5173, and the API picks up OpenTelemetry,
  health checks and service discovery from ServiceDefaults. /health and /alive
  now answer in development.
- A Husky pre-push hook runs scripts/ci/prepush.sh: build, test, then a web
  build. The scripts are plain bash so CI can run the same steps.
- The MCP server's env var is now NOVELLY_API_URL.

Verified beyond the build: 44 tests pass, the web client builds, the API was
exercised over curl (project/chapter/beat/tag round trip, tag cross-reference,
503 on the agent without a key while conversation listing still returns 200),
the MCP server was driven over stdio JSON-RPC (26 tools, errors still surface
the API's own message rather than being flattened), and the AppHost was run to
confirm both resources come up and Vite proxies /api through to the API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
This commit is contained in:
James Wampler
2026-08-06 12:11:20 -07:00
co-authored by Claude Opus 5
parent 30e0c6926e
commit 725758ccd9
120 changed files with 811 additions and 421 deletions
+62
View File
@@ -0,0 +1,62 @@
using Novelly.Api.Common;
namespace Novelly.Api.Scenes;
public record SceneDto(
Guid Id,
Guid ChapterId,
int SortOrder,
string Title,
string? Summary,
string? Goal,
string? Conflict,
string? Outcome,
Guid? PovCharacterId,
string? PovCharacterName,
string? Location,
string? Prose,
int WordCount,
DraftStatus Status,
DateTimeOffset UpdatedAt);
public record CreateSceneRequest(
string Title,
int? SortOrder = null,
string? Summary = null,
string? Goal = null,
string? Conflict = null,
string? Outcome = null,
Guid? PovCharacterId = null,
string? Location = null,
string? Prose = null,
DraftStatus Status = DraftStatus.Planned);
public record UpdateSceneRequest(
string? Title = null,
int? SortOrder = null,
string? Summary = null,
string? Goal = null,
string? Conflict = null,
string? Outcome = null,
Guid? PovCharacterId = null,
string? Location = null,
string? Prose = null,
DraftStatus? Status = null);
public static class SceneMapping
{
public static SceneDto ToDto(this Scene s) => new(
s.Id, s.ChapterId, s.SortOrder, s.Title, s.Summary,
s.Goal, s.Conflict, s.Outcome,
s.PovCharacterId, s.PovCharacter?.Name, s.Location,
s.Prose, s.WordCount, s.Status, s.UpdatedAt);
/// <summary>
/// Whitespace-delimited word count. Good enough for progress tracking, and it costs
/// nothing to recompute on every save.
/// </summary>
public static int CountWords(string? prose) =>
string.IsNullOrWhiteSpace(prose)
? 0
: prose.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
}