561 lines
25 KiB
C#
561 lines
25 KiB
C#
using System.Text.Json;
|
|
using PlotLine.Models;
|
|
using PlotLine.Models.StoryIntelligence;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IStorySceneValidator
|
|
{
|
|
ValidationResult Validate(SceneIntelligenceScene? scene);
|
|
}
|
|
|
|
public sealed class StorySceneValidator : IStorySceneValidator
|
|
{
|
|
private static readonly HashSet<string> LocationTypes = CreateSet("named", "generic", "unclear");
|
|
private static readonly HashSet<string> CharacterRoles = CreateSet("present", "pointOfView", "speaker", "mentioned", "unclear");
|
|
private static readonly HashSet<string> KnowledgeChangeTypes = CreateSet("Learns", "Realises", "IsTold", "Discovers", "Confirms", "Suspects", "Misunderstands");
|
|
private static readonly HashSet<string> ObservationTypes = CreateSet("CharacterAction", "CharacterKnowledge", "CharacterLocation", "RelationshipSignal", "AssetInteraction", "LocationSignal", "TimelineSignal", "QuestionSignal");
|
|
private static readonly HashSet<string> EntityTypes = CreateSet("Character", "Location", "Asset", "Knowledge", "Relationship", "Timeline", "Scene", "Unknown");
|
|
private static readonly HashSet<string> PreferredMetrics = CreateSet("action", "emotion", "tension", "conflict", "mystery", "romance", "humour", "revelation", "pacingIntensity");
|
|
private static readonly HashSet<string> RelationshipSignals = CreateSet("trust", "mistrust", "affection", "fear", "authority", "secrecy", "conflict", "dependence", "protection", "rivalry", "obligation");
|
|
|
|
public ValidationResult Validate(SceneIntelligenceScene? scene)
|
|
{
|
|
var result = new ValidationResult();
|
|
|
|
try
|
|
{
|
|
if (scene is null)
|
|
{
|
|
AddError(result, "$", "Scene Intelligence JSON could not be deserialised into the V1 model.", "Return one valid Scene Intelligence JSON object.");
|
|
return result;
|
|
}
|
|
|
|
ValidateRoot(scene, result);
|
|
ValidateSceneReference(scene.SceneReference, result);
|
|
ValidateSummary(scene.Summary, result);
|
|
ValidateScenePurpose(scene.ScenePurpose, result);
|
|
ValidatePointOfView(scene.PointOfView, result);
|
|
ValidateSetting(scene.Setting, result);
|
|
ValidateCharacters(scene.Characters, result);
|
|
ValidateLocations(scene.Locations, result);
|
|
ValidateAssets(scene.Assets, result);
|
|
ValidateRelationships(scene.Relationships, result);
|
|
ValidateKnowledgeChanges(scene.KnowledgeChanges, result);
|
|
ValidateTimelineClues(scene.TimelineClues, result);
|
|
ValidateQuestionsRaised(scene.QuestionsRaised, result);
|
|
ValidateQuestionsAnswered(scene.QuestionsAnswered, result);
|
|
ValidateObservations(scene.Observations, result);
|
|
ValidateMetrics(scene.Metrics, result);
|
|
ValidateSourceLimits(scene.SourceLimits, 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 ValidateRoot(SceneIntelligenceScene scene, ValidationResult result)
|
|
{
|
|
RequiredString(scene.SchemaVersion, "schemaVersion", result);
|
|
if (!string.Equals(scene.SchemaVersion, "1.0", StringComparison.Ordinal))
|
|
{
|
|
AddError(result, "schemaVersion", "Schema version must be \"1.0\".", "Return schemaVersion as \"1.0\".");
|
|
}
|
|
|
|
RequiredObject(scene.SceneReference, "sceneReference", result);
|
|
RequiredObject(scene.Summary, "summary", result);
|
|
RequiredObject(scene.ScenePurpose, "scenePurpose", result);
|
|
RequiredObject(scene.PointOfView, "pointOfView", result);
|
|
RequiredObject(scene.Setting, "setting", result);
|
|
RequiredArray(scene.Characters, "characters", result);
|
|
RequiredArray(scene.Locations, "locations", result);
|
|
RequiredArray(scene.Assets, "assets", result);
|
|
RequiredArray(scene.Relationships, "relationships", result);
|
|
RequiredArray(scene.KnowledgeChanges, "knowledgeChanges", result);
|
|
RequiredArray(scene.TimelineClues, "timelineClues", result);
|
|
RequiredArray(scene.QuestionsRaised, "questionsRaised", result);
|
|
RequiredArray(scene.QuestionsAnswered, "questionsAnswered", result);
|
|
RequiredArray(scene.Observations, "observations", result);
|
|
RequiredObject(scene.Metrics, "metrics", result);
|
|
RequiredObject(scene.SourceLimits, "sourceLimits", result);
|
|
Unknown(scene.ExtensionData, "$", result);
|
|
}
|
|
|
|
private static void ValidateSceneReference(SceneIntelligenceSceneReference? value, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RequiredScalar(value.ProjectId, "sceneReference.projectId", result, allowNull: true);
|
|
RequiredScalar(value.BookId, "sceneReference.bookId", result, allowNull: true);
|
|
RequiredScalar(value.ChapterId, "sceneReference.chapterId", result, allowNull: true);
|
|
RequiredScalar(value.SceneId, "sceneReference.sceneId", result, allowNull: true);
|
|
RequiredScalar(value.ChapterNumber, "sceneReference.chapterNumber", result, allowNull: true);
|
|
RequiredScalar(value.SceneNumber, "sceneReference.sceneNumber", result, allowNull: true);
|
|
RequiredString(value.SourceLabel, "sceneReference.sourceLabel", result);
|
|
Unknown(value.ExtensionData, "sceneReference", result);
|
|
}
|
|
|
|
private static void ValidateSummary(SceneIntelligenceSummary? value, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RequiredString(value.Short, "summary.short", result);
|
|
RequiredString(value.Detailed, "summary.detailed", result);
|
|
Confidence(value.Confidence, "summary.confidence", result);
|
|
Unknown(value.ExtensionData, "summary", result);
|
|
}
|
|
|
|
private static void ValidateScenePurpose(SceneIntelligenceScenePurpose? value, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RequiredString(value.ObservedFunction, "scenePurpose.observedFunction", result);
|
|
Confidence(value.Confidence, "scenePurpose.confidence", result);
|
|
Unknown(value.ExtensionData, "scenePurpose", result);
|
|
}
|
|
|
|
private static void ValidatePointOfView(SceneIntelligencePointOfView? value, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RequiredScalar(value.CharacterName, "pointOfView.characterName", result, allowNull: true);
|
|
RequiredScalar(value.NarrativeMode, "pointOfView.narrativeMode", result, allowNull: true);
|
|
Confidence(value.Confidence, "pointOfView.confidence", result);
|
|
RequiredScalar(value.Evidence, "pointOfView.evidence", result, allowNull: true);
|
|
Unknown(value.ExtensionData, "pointOfView", result);
|
|
}
|
|
|
|
private static void ValidateSetting(SceneIntelligenceSetting? value, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RequiredScalar(value.TimeOfDay, "setting.timeOfDay", result, allowNull: true);
|
|
RequiredScalar(value.DateOrTimeReference, "setting.dateOrTimeReference", result, allowNull: true);
|
|
RequiredScalar(value.LocationName, "setting.locationName", result, allowNull: true);
|
|
Enum(value.LocationType, "setting.locationType", LocationTypes, result);
|
|
RequiredScalar(value.GenericRoomType, "setting.genericRoomType", result, allowNull: true);
|
|
RequiredScalar(value.ParentLocationHint, "setting.parentLocationHint", result, allowNull: true);
|
|
Confidence(value.Confidence, "setting.confidence", result);
|
|
|
|
if (string.Equals(value.LocationType, "generic", StringComparison.Ordinal) && string.IsNullOrWhiteSpace(value.GenericRoomType))
|
|
{
|
|
AddWarning(result, "setting.genericRoomType", "Generic setting has no generic room type.", "Include genericRoomType when the manuscript supports it.");
|
|
}
|
|
|
|
Unknown(value.ExtensionData, "setting", result);
|
|
}
|
|
|
|
private static void ValidateCharacters(List<SceneIntelligenceCharacter>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DuplicateNames(values.Select((item, index) => (item.Name, Path: $"characters[{index}].name")), "characters", result);
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"characters[{i}]";
|
|
RequiredString(item.Name, $"{path}.name", result);
|
|
Enum(item.RoleInScene, $"{path}.roleInScene", CharacterRoles, result);
|
|
RequiredScalar(item.MentionedOnly, $"{path}.mentionedOnly", result);
|
|
RequiredArray(item.Aliases, $"{path}.aliases", result);
|
|
RequiredArray(item.Actions, $"{path}.actions", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
RequiredScalar(item.Notes, $"{path}.notes", result, allowNull: true);
|
|
if (item.MentionedOnly == true && !string.Equals(item.RoleInScene, "mentioned", StringComparison.Ordinal))
|
|
{
|
|
AddWarning(result, $"{path}.roleInScene", "Mentioned-only character should usually use roleInScene \"mentioned\".", "Use roleInScene \"mentioned\" for absent references.");
|
|
}
|
|
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateLocations(List<SceneIntelligenceLocation>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DuplicateNames(values.Select((item, index) => (item.Name, Path: $"locations[{index}].name")), "locations", result);
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"locations[{i}]";
|
|
RequiredString(item.Name, $"{path}.name", result);
|
|
Enum(item.LocationType, $"{path}.locationType", LocationTypes, result);
|
|
RequiredScalar(item.GenericRoomType, $"{path}.genericRoomType", result, allowNull: true);
|
|
RequiredScalar(item.ParentLocationHint, $"{path}.parentLocationHint", result, allowNull: true);
|
|
RequiredScalar(item.PresentInScene, $"{path}.presentInScene", result);
|
|
RequiredScalar(item.MentionedOnly, $"{path}.mentionedOnly", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
RequiredScalar(item.Notes, $"{path}.notes", result, allowNull: true);
|
|
if (item.PresentInScene == false && item.MentionedOnly == false)
|
|
{
|
|
AddWarning(result, $"{path}.mentionedOnly", "Location is not present but is not marked mentioned-only.", "Set mentionedOnly to true when a location is only discussed.");
|
|
}
|
|
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateAssets(List<SceneIntelligenceAsset>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DuplicateNames(values.Select((item, index) => (item.Name, Path: $"assets[{index}].name")), "assets", result);
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"assets[{i}]";
|
|
RequiredString(item.Name, $"{path}.name", result);
|
|
RequiredScalar(item.AssetType, $"{path}.assetType", result, allowNull: true);
|
|
RequiredScalar(item.Status, $"{path}.status", result, allowNull: true);
|
|
RequiredScalar(item.OwnerOrHolder, $"{path}.ownerOrHolder", result, allowNull: true);
|
|
RequiredScalar(item.MentionedOnly, $"{path}.mentionedOnly", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
RequiredScalar(item.Notes, $"{path}.notes", result, allowNull: true);
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateRelationships(List<SceneIntelligenceRelationship>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"relationships[{i}]";
|
|
RequiredString(item.CharacterA, $"{path}.characterA", result);
|
|
RequiredString(item.CharacterB, $"{path}.characterB", result);
|
|
RequiredString(item.RelationshipSignal, $"{path}.relationshipSignal", result);
|
|
if (!string.IsNullOrWhiteSpace(item.RelationshipSignal) && !RelationshipSignals.Contains(item.RelationshipSignal))
|
|
{
|
|
AddWarning(result, $"{path}.relationshipSignal", "Relationship signal is outside the preferred V1 neutral terms.", "Use a supported neutral signal when one fits.");
|
|
}
|
|
|
|
RequiredString(item.Evidence, $"{path}.evidence", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
var key = string.Join("|", new[] { item.CharacterA, item.CharacterB, item.RelationshipSignal }.Select(value => value?.Trim().ToLowerInvariant()));
|
|
if (!string.IsNullOrWhiteSpace(key) && !seen.Add(key))
|
|
{
|
|
AddWarning(result, path, "Duplicate relationship signal found.", "Merge duplicate scene-local relationship signals when appropriate.");
|
|
}
|
|
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateKnowledgeChanges(List<SceneIntelligenceKnowledgeChange>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"knowledgeChanges[{i}]";
|
|
RequiredString(item.RecipientCharacter, $"{path}.recipientCharacter", result);
|
|
RequiredString(item.KnowledgeItem, $"{path}.knowledgeItem", result);
|
|
Enum(item.ChangeType, $"{path}.changeType", KnowledgeChangeTypes, result);
|
|
RequiredScalar(item.SourceCharacter, $"{path}.sourceCharacter", result, allowNull: true);
|
|
RequiredScalar(item.SourceType, $"{path}.sourceType", result, allowNull: true);
|
|
RequiredString(item.Evidence, $"{path}.evidence", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateTimelineClues(List<SceneIntelligenceTimelineClue>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"timelineClues[{i}]";
|
|
RequiredString(item.Clue, $"{path}.clue", result);
|
|
RequiredScalar(item.RelativeOrder, $"{path}.relativeOrder", result, allowNull: true);
|
|
RequiredScalar(item.AbsoluteDate, $"{path}.absoluteDate", result, allowNull: true);
|
|
RequiredString(item.Evidence, $"{path}.evidence", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateQuestionsRaised(List<SceneIntelligenceQuestionRaised>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"questionsRaised[{i}]";
|
|
RequiredString(item.Question, $"{path}.question", result);
|
|
RequiredString(item.Scope, $"{path}.scope", result);
|
|
RequiredString(item.Evidence, $"{path}.evidence", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateQuestionsAnswered(List<SceneIntelligenceQuestionAnswered>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"questionsAnswered[{i}]";
|
|
RequiredString(item.Question, $"{path}.question", result);
|
|
RequiredString(item.Answer, $"{path}.answer", result);
|
|
RequiredString(item.Evidence, $"{path}.evidence", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateObservations(List<SceneIntelligenceObservation>? values, ValidationResult result)
|
|
{
|
|
if (values is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var item = values[i];
|
|
var path = $"observations[{i}]";
|
|
Enum(item.ObservationType, $"{path}.observationType", ObservationTypes, result);
|
|
Enum(item.SubjectEntityType, $"{path}.subjectEntityType", EntityTypes, result);
|
|
RequiredString(item.SubjectName, $"{path}.subjectName", result);
|
|
NullableEnum(item.ObjectEntityType, $"{path}.objectEntityType", EntityTypes, result);
|
|
RequiredScalar(item.ObjectName, $"{path}.objectName", result, allowNull: true);
|
|
RequiredString(item.Predicate, $"{path}.predicate", result);
|
|
RequiredString(item.Description, $"{path}.description", result);
|
|
RequiredString(item.Evidence, $"{path}.evidence", result);
|
|
Confidence(item.Confidence, $"{path}.confidence", result);
|
|
if (string.IsNullOrWhiteSpace(item.ObjectEntityType) != string.IsNullOrWhiteSpace(item.ObjectName))
|
|
{
|
|
AddWarning(result, path, "Observation object entity type and object name are inconsistent.", "Use both objectEntityType and objectName, or set both to null when no object applies.");
|
|
}
|
|
|
|
Unknown(item.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateMetrics(Dictionary<string, SceneIntelligenceMetric>? metrics, ValidationResult result)
|
|
{
|
|
if (metrics is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var (name, metric) in metrics)
|
|
{
|
|
var path = $"metrics.{name}";
|
|
if (!PreferredMetrics.Contains(name))
|
|
{
|
|
AddWarning(result, path, "Metric is outside the preferred V1 metric set.", "Use preferred V1 metrics unless the schema is intentionally expanded.");
|
|
}
|
|
|
|
if (metric is null)
|
|
{
|
|
AddError(result, path, "Metric object is required when a metric is present.", "Return { \"score\": 1-10, \"confidence\": 0.0-1.0 }.");
|
|
continue;
|
|
}
|
|
|
|
if (!metric.Score.HasValue)
|
|
{
|
|
AddError(result, $"{path}.score", "Metric score is required.", "Return a score from 1 to 10.");
|
|
}
|
|
else if (metric.Score < 1 || metric.Score > 10)
|
|
{
|
|
AddError(result, $"{path}.score", "Metric score must be from 1 to 10.", "Use the V1 1-10 descriptive intensity scale.");
|
|
}
|
|
|
|
Confidence(metric.Confidence, $"{path}.confidence", result);
|
|
Unknown(metric.ExtensionData, path, result);
|
|
}
|
|
}
|
|
|
|
private static void ValidateSourceLimits(SceneIntelligenceSourceLimits? value, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RequiredScalar(value.ContainsAmbiguity, "sourceLimits.containsAmbiguity", result);
|
|
RequiredArray(value.AmbiguityNotes, "sourceLimits.ambiguityNotes", result);
|
|
Unknown(value.ExtensionData, "sourceLimits", result);
|
|
}
|
|
|
|
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 V1 confidence scale.");
|
|
}
|
|
}
|
|
|
|
private static void Enum(string? value, string path, HashSet<string> allowed, ValidationResult result)
|
|
{
|
|
RequiredString(value, path, result);
|
|
if (!string.IsNullOrWhiteSpace(value) && !allowed.Contains(value))
|
|
{
|
|
AddError(result, path, $"Value \"{value}\" is not allowed.", $"Use one of: {string.Join(", ", allowed)}.");
|
|
}
|
|
}
|
|
|
|
private static void NullableEnum(string? value, string path, HashSet<string> allowed, ValidationResult result)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(value) && !allowed.Contains(value))
|
|
{
|
|
AddError(result, path, $"Value \"{value}\" is not allowed.", $"Use null or one of: {string.Join(", ", allowed)}.");
|
|
}
|
|
}
|
|
|
|
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 scene.");
|
|
}
|
|
}
|
|
|
|
private static void RequiredScalar<T>(T? value, string path, ValidationResult result, bool allowNull = false)
|
|
{
|
|
if (value is null && !allowNull)
|
|
{
|
|
AddError(result, path, "Required property is missing or null.", "Return the required property with a supported value.");
|
|
}
|
|
}
|
|
|
|
private static void RequiredObject<T>(T? value, string path, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
AddError(result, path, "Required object is missing or null.", "Return the required V1 object.");
|
|
}
|
|
}
|
|
|
|
private static void RequiredArray<T>(List<T>? value, string path, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
AddError(result, path, "Required array is missing or null.", "Return an empty array when no items are supported.");
|
|
}
|
|
}
|
|
|
|
private static void RequiredObject<TValue>(Dictionary<string, TValue>? value, string path, ValidationResult result)
|
|
{
|
|
if (value is null)
|
|
{
|
|
AddError(result, path, "Required object is missing or null.", "Return an empty object when no metrics are supported.");
|
|
}
|
|
}
|
|
|
|
private static void DuplicateNames(IEnumerable<(string? Name, string Path)> values, string collectionPath, ValidationResult result)
|
|
{
|
|
var seen = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var (name, path) in values)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var key = name.Trim();
|
|
if (seen.TryGetValue(key, out var existingPath))
|
|
{
|
|
AddWarning(result, path, $"Duplicate entity name also appears at {existingPath}.", $"Avoid duplicate names in {collectionPath} unless they represent distinct supported entities.");
|
|
}
|
|
else
|
|
{
|
|
seen[key] = path;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 Scene Intelligence Schema 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 });
|
|
|
|
private static HashSet<string> CreateSet(params string[] values)
|
|
=> new(values, StringComparer.Ordinal);
|
|
}
|