180 lines
7.5 KiB
C#
180 lines
7.5 KiB
C#
using System.Text.Json;
|
|
using PlotLine.Models;
|
|
using PlotLine.Models.StoryIntelligence;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IChapterStructureValidator
|
|
{
|
|
ValidationResult Validate(ChapterStructureModel? chapterStructure, int paragraphCount);
|
|
}
|
|
|
|
public sealed class ChapterStructureValidator : IChapterStructureValidator
|
|
{
|
|
public ValidationResult Validate(ChapterStructureModel? chapterStructure, int paragraphCount)
|
|
{
|
|
var result = new ValidationResult();
|
|
|
|
try
|
|
{
|
|
if (chapterStructure is null)
|
|
{
|
|
AddError(result, "$", "Chapter Structure JSON could not be deserialised.", "Return one valid Chapter Structure JSON object.");
|
|
return result;
|
|
}
|
|
|
|
if (paragraphCount <= 0)
|
|
{
|
|
AddError(result, "chapterText", "Chapter text must contain at least one non-empty paragraph.", "Enter chapter text with one or more paragraphs.");
|
|
}
|
|
|
|
RequiredString(chapterStructure.SchemaVersion, "schemaVersion", result);
|
|
if (!string.Equals(chapterStructure.SchemaVersion, "1.0", StringComparison.Ordinal))
|
|
{
|
|
AddError(result, "schemaVersion", "Schema version must be \"1.0\".", "Return schemaVersion as \"1.0\".");
|
|
}
|
|
|
|
RequiredString(chapterStructure.ChapterSummary, "chapterSummary", result);
|
|
if (chapterStructure.SceneBoundaries is null)
|
|
{
|
|
AddError(result, "sceneBoundaries", "Scene boundaries array is required.", "Return sceneBoundaries as an array.");
|
|
}
|
|
else
|
|
{
|
|
ValidateBoundaries(chapterStructure.SceneBoundaries, paragraphCount, result);
|
|
}
|
|
|
|
Unknown(chapterStructure.ExtensionData, "$", result);
|
|
|
|
if (result.IsValid)
|
|
{
|
|
AddInfo(result, "$", "Validation passed.", "No action needed.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AddError(result, "$", $"Validator failed internally: {ex.Message}", "Review validator implementation for this response shape.");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void ValidateBoundaries(List<ChapterSceneBoundary> boundaries, int paragraphCount, ValidationResult result)
|
|
{
|
|
if (boundaries.Count == 0)
|
|
{
|
|
AddError(result, "sceneBoundaries", "At least one scene boundary is required.", "Return one boundary covering the chapter if no scene split is supported.");
|
|
return;
|
|
}
|
|
|
|
var expectedStart = 1;
|
|
for (var i = 0; i < boundaries.Count; i++)
|
|
{
|
|
var boundary = boundaries[i];
|
|
var path = $"sceneBoundaries[{i}]";
|
|
var expectedSceneNumber = i + 1;
|
|
|
|
if (!boundary.SceneNumber.HasValue)
|
|
{
|
|
AddError(result, $"{path}.sceneNumber", "Scene number is required.", $"Return sceneNumber {expectedSceneNumber}.");
|
|
}
|
|
else if (boundary.SceneNumber.Value != expectedSceneNumber)
|
|
{
|
|
AddError(result, $"{path}.sceneNumber", "Scene numbers must be sequential from 1.", $"Use sceneNumber {expectedSceneNumber} for this boundary.");
|
|
}
|
|
|
|
if (!boundary.StartParagraph.HasValue)
|
|
{
|
|
AddError(result, $"{path}.startParagraph", "Start paragraph is required.", "Return the first paragraph number for this boundary.");
|
|
}
|
|
|
|
if (!boundary.EndParagraph.HasValue)
|
|
{
|
|
AddError(result, $"{path}.endParagraph", "End paragraph is required.", "Return the last paragraph number for this boundary.");
|
|
}
|
|
|
|
if (boundary.StartParagraph.HasValue && boundary.EndParagraph.HasValue)
|
|
{
|
|
if (boundary.StartParagraph.Value > boundary.EndParagraph.Value)
|
|
{
|
|
AddError(result, path, "Start paragraph cannot be after end paragraph.", "Use an ordered inclusive paragraph range.");
|
|
}
|
|
|
|
if (boundary.StartParagraph.Value != expectedStart)
|
|
{
|
|
AddError(result, $"{path}.startParagraph", "Scene boundaries must not overlap or leave gaps.", $"Use startParagraph {expectedStart}.");
|
|
}
|
|
|
|
if (boundary.StartParagraph.Value < 1 || boundary.EndParagraph.Value < 1)
|
|
{
|
|
AddError(result, path, "Paragraph numbers must be 1 or greater.", "Use paragraph numbers from the submitted chapter text.");
|
|
}
|
|
|
|
if (paragraphCount > 0 && boundary.EndParagraph.Value > paragraphCount)
|
|
{
|
|
AddError(result, $"{path}.endParagraph", "End paragraph exceeds the submitted chapter paragraph count.", $"Use a value from 1 to {paragraphCount}.");
|
|
}
|
|
|
|
expectedStart = boundary.EndParagraph.Value + 1;
|
|
}
|
|
|
|
Confidence(boundary.Confidence, $"{path}.confidence", result);
|
|
RequiredString(boundary.Reason, $"{path}.reason", result);
|
|
Unknown(boundary.ExtensionData, path, result);
|
|
}
|
|
|
|
if (paragraphCount > 0 && expectedStart <= paragraphCount)
|
|
{
|
|
AddError(result, "sceneBoundaries", "Scene boundaries do not cover the full chapter.", $"Add a boundary ending at paragraph {paragraphCount}.");
|
|
}
|
|
}
|
|
|
|
private static void Confidence(decimal? value, string path, ValidationResult result)
|
|
{
|
|
if (!value.HasValue)
|
|
{
|
|
AddError(result, path, "Confidence is required.", "Return a confidence value from 0.0 to 1.0.");
|
|
return;
|
|
}
|
|
|
|
if (value < 0 || value > 1)
|
|
{
|
|
AddError(result, path, "Confidence must be from 0.0 to 1.0.", "Use the 0.0 to 1.0 confidence scale.");
|
|
}
|
|
}
|
|
|
|
private static void RequiredString(string? value, string path, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
AddError(result, path, "Required string property is missing or null.", "Return a supported string value.");
|
|
}
|
|
else if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
AddError(result, path, "Required string property is empty.", "Return a non-empty string supported by the chapter structure.");
|
|
}
|
|
}
|
|
|
|
private static void Unknown(Dictionary<string, JsonElement>? values, string path, ValidationResult result)
|
|
{
|
|
if (values is null || values.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var propertyName in values.Keys.Order(StringComparer.Ordinal))
|
|
{
|
|
AddWarning(result, path == "$" ? propertyName : $"{path}.{propertyName}", "Unknown property is not part of Chapter Structure V1.", "Remove this property or update the schema version intentionally.");
|
|
}
|
|
}
|
|
|
|
private static void AddError(ValidationResult result, string path, string message, string suggestedFix)
|
|
=> result.Errors.Add(new ValidationIssue { Severity = "Error", Path = path, Message = message, SuggestedFix = suggestedFix });
|
|
|
|
private static void AddWarning(ValidationResult result, string path, string message, string suggestedFix)
|
|
=> result.Warnings.Add(new ValidationIssue { Severity = "Warning", Path = path, Message = message, SuggestedFix = suggestedFix });
|
|
|
|
private static void AddInfo(ValidationResult result, string path, string message, string suggestedFix)
|
|
=> result.Information.Add(new ValidationIssue { Severity = "Information", Path = path, Message = message, SuggestedFix = suggestedFix });
|
|
}
|