Add outline import feature; drop Dto naming, map entities at the API boundary

Services now return entities; endpoints (and the agent toolsets) map to
*Response records instead of services building wire DTOs themselves.
Also brings in the outline-import agent, MCP tool, ledger and web dialog
that were already in progress on disk.
This commit is contained in:
James Wampler
2026-08-06 18:36:40 -07:00
parent 40f93e40a8
commit 189ebf3237
66 changed files with 3310 additions and 364 deletions
@@ -0,0 +1,78 @@
using Novelly.Api.Common.Validation;
namespace Novelly.Api.Imports;
public record ImportJobResponse(
Guid Id,
string SourceRoot,
Guid? ProjectId,
ImportJobStatus Status,
string? StatusMessage,
int ChaptersCompleted,
int ChaptersTotal,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
/// <summary>Whether a source folder is ready for a fresh import, has one to resume, or is already done.</summary>
public enum ImportReadiness
{
Fresh,
Resumable,
Complete
}
public record ImportInspectionResponse(
ImportReadiness Readiness,
Guid? ProjectId,
int ChaptersCompleted,
int ChaptersTotal,
IReadOnlyList<string> CompletedPasses);
public record InspectImportRequest(string SourceRoot);
public class InspectImportRequestValidator : IModelValidator<InspectImportRequest>
{
public ValidationResult Validate(InspectImportRequest model)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(model.SourceRoot))
result.AddError("SourceRoot", "'Source Root' must not be empty.");
return result;
}
}
/// <summary>
/// Starts a fresh import, resumes an incomplete one, or — with <see cref="ForceRestart"/> —
/// deletes the ledger and the project it points at before starting clean. Resuming needs no
/// flag: the importer always continues from the ledger it finds unless told to wipe it.
/// </summary>
public record StartImportRequest(string SourceRoot, bool ForceRestart = false);
public class StartImportRequestValidator : IModelValidator<StartImportRequest>
{
public ValidationResult Validate(StartImportRequest model)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(model.SourceRoot))
result.AddError("SourceRoot", "'Source Root' must not be empty.");
return result;
}
}
public static class ImportMapping
{
public static ImportJobResponse ToResponse(this ImportJob job) => new(
job.Id,
job.SourceRoot,
job.ProjectId,
job.Status,
job.StatusMessage,
job.ChaptersCompleted,
job.ChaptersTotal,
job.CreatedAt,
job.UpdatedAt);
}