Import Phase 1F
This commit is contained in:
parent
ed341587de
commit
b684146248
@ -13,7 +13,7 @@ public interface IImportRepository
|
||||
|
||||
public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IImportRepository
|
||||
{
|
||||
private const string ImportMarker = "Imported via JSON Import Phase 1E";
|
||||
private const string ImportMarker = "Imported via JSON Import Phase 1F";
|
||||
|
||||
public async Task<bool> ProjectTitleExistsAsync(string projectTitle)
|
||||
{
|
||||
@ -50,6 +50,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
var assetStatesCreated = 0;
|
||||
var assetEventsCreated = 0;
|
||||
var assetCustodyEventsCreated = 0;
|
||||
var sceneDependenciesCreated = 0;
|
||||
var skippedFields = GetSkippedOptionalFields(package);
|
||||
var unresolvedWarnings = GetUnresolvedWarnings(package);
|
||||
var sceneMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
@ -491,6 +492,45 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
}
|
||||
}
|
||||
|
||||
var importedSceneDependencyKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var dependency in package.SceneDependencies)
|
||||
{
|
||||
if (!TryResolveSceneReference(sceneMap, dependency.FromScene, out var sourceSceneId)
|
||||
|| !TryResolveSceneReference(sceneMap, dependency.ToScene, out var targetSceneId)
|
||||
|| sourceSceneId == targetSceneId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dependencyTypeId = ResolveSceneDependencyTypeId(lookupIds, dependency.DependencyType);
|
||||
var dependencyKey = $"{sourceSceneId}/{targetSceneId}/{dependencyTypeId}";
|
||||
if (!importedSceneDependencyKeys.Add(dependencyKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await connection.QuerySingleAsync<int>(
|
||||
"dbo.SceneDependency_Save",
|
||||
new
|
||||
{
|
||||
SceneDependencyID = 0,
|
||||
ProjectID = projectId,
|
||||
SourceSceneID = sourceSceneId,
|
||||
TargetSceneID = targetSceneId,
|
||||
SceneDependencyTypeID = dependencyTypeId,
|
||||
Description = CombineText(
|
||||
dependency.Description,
|
||||
MetadataLine("Dependency type", dependency.DependencyType),
|
||||
MetadataLine("Strength", dependency.Strength),
|
||||
dependency.Notes,
|
||||
ImportMarker),
|
||||
IsHardDependency = ResolveSceneDependencyStrength(dependency.Strength)
|
||||
},
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
sceneDependenciesCreated++;
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
return new ImportResult
|
||||
{
|
||||
@ -510,6 +550,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
AssetStatesCreated = assetStatesCreated,
|
||||
AssetEventsCreated = assetEventsCreated,
|
||||
AssetCustodyEventsCreated = assetCustodyEventsCreated,
|
||||
SceneDependenciesCreated = sceneDependenciesCreated,
|
||||
SkippedOptionalFields = skippedFields,
|
||||
UnresolvedWarnings = unresolvedWarnings,
|
||||
Message = "Import complete."
|
||||
@ -555,6 +596,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
var threadEventTypes = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT ThreadEventTypeID AS LookupID, TypeName FROM dbo.ThreadEventTypes WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var sceneDependencyTypes = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT SceneDependencyTypeID AS LookupID, TypeName FROM dbo.SceneDependencyTypes WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
var assetKinds = (await connection.QueryAsync<ImportLookupRow>(
|
||||
"SELECT AssetKindID AS LookupID, KindName AS TypeName FROM dbo.AssetKinds WHERE IsActive = 1;",
|
||||
transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase);
|
||||
@ -587,6 +631,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
threadTypes,
|
||||
threadStatuses,
|
||||
threadEventTypes,
|
||||
sceneDependencyTypes,
|
||||
assetKinds,
|
||||
assetStates,
|
||||
assetEventTypes,
|
||||
@ -640,6 +685,11 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
skipped.Add("Asset event character names were stored in event descriptions.");
|
||||
}
|
||||
|
||||
if (package.SceneDependencies.Any(dependency => !string.IsNullOrWhiteSpace(dependency.DependencyType) || !string.IsNullOrWhiteSpace(dependency.Strength)))
|
||||
{
|
||||
skipped.Add("Scene dependency type and strength labels were mapped to existing dependency fields and preserved in descriptions.");
|
||||
}
|
||||
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@ -729,6 +779,21 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
warnings.Add($"Unresolved asset event scene reference: {reference}");
|
||||
}
|
||||
|
||||
foreach (var reference in package.SceneDependencies.SelectMany((dependency, index) => new[]
|
||||
{
|
||||
new { Label = $"Dependency {index + 1} fromScene", Reference = dependency.FromScene },
|
||||
new { Label = $"Dependency {index + 1} toScene", Reference = dependency.ToScene }
|
||||
})
|
||||
.Where(x => x.Reference?.BookOrder.HasValue != true
|
||||
|| !x.Reference.ChapterOrder.HasValue
|
||||
|| !x.Reference.SceneOrder.HasValue
|
||||
|| !sceneKeys.Contains(SceneKey(x.Reference.BookOrder.GetValueOrDefault(), x.Reference.ChapterOrder.GetValueOrDefault(), x.Reference.SceneOrder.GetValueOrDefault())))
|
||||
.Select(x => $"{x.Label} -> {SceneReferenceKey(x.Reference)}")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
warnings.Add($"Unresolved scene dependency reference: {reference}");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
@ -863,6 +928,48 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
: lookupIds.ThreadEventTypeIds.Values.First();
|
||||
}
|
||||
|
||||
private static int ResolveSceneDependencyTypeId(ImportLookupIds lookupIds, string? dependencyType)
|
||||
{
|
||||
var normalized = Clean(dependencyType);
|
||||
if (normalized.Length > 0 && lookupIds.SceneDependencyTypeIds.TryGetValue(normalized, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
var mappedType = normalized.ToLowerInvariant() switch
|
||||
{
|
||||
"causes" or "requires" or "blocks" or "follows" => "Must Occur Before",
|
||||
"reveals" or "foreshadows" or "paysoff" or "pays off" => "Reveal Setup",
|
||||
"contradicts" => "Knowledge Dependency",
|
||||
"mirrors" => "Emotional Setup",
|
||||
_ => "Must Occur Before"
|
||||
};
|
||||
|
||||
return lookupIds.SceneDependencyTypeIds.TryGetValue(mappedType, out var mappedId)
|
||||
? mappedId
|
||||
: lookupIds.SceneDependencyTypeIds.Values.First();
|
||||
}
|
||||
|
||||
private static bool ResolveSceneDependencyStrength(string? strength)
|
||||
{
|
||||
var normalized = Clean(strength);
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (int.TryParse(normalized, out var numeric))
|
||||
{
|
||||
return numeric >= 7;
|
||||
}
|
||||
|
||||
return normalized.Equals("Hard", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("Strong", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("Required", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("High", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("Critical", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static int ResolveAssetKindId(ImportLookupIds lookupIds, string? type)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(type) && lookupIds.AssetKindIds.TryGetValue(type.Trim(), out var id))
|
||||
@ -955,9 +1062,25 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveSceneReference(IReadOnlyDictionary<string, int> sceneMap, ImportSceneReferenceDto? reference, out int sceneId)
|
||||
{
|
||||
if (reference?.BookOrder.HasValue == true && reference.ChapterOrder.HasValue && reference.SceneOrder.HasValue)
|
||||
{
|
||||
return sceneMap.TryGetValue(SceneKey(reference.BookOrder.Value, reference.ChapterOrder.Value, reference.SceneOrder.Value), out sceneId);
|
||||
}
|
||||
|
||||
sceneId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string SceneKey(int bookOrder, int chapterOrder, int sceneOrder)
|
||||
=> $"{bookOrder}/{chapterOrder}/{sceneOrder}";
|
||||
|
||||
private static string SceneReferenceKey(ImportSceneReferenceDto? reference)
|
||||
=> reference is null
|
||||
? "(missing)"
|
||||
: $"{reference.BookOrder?.ToString() ?? "?"}/{reference.ChapterOrder?.ToString() ?? "?"}/{reference.SceneOrder?.ToString() ?? "?"}";
|
||||
|
||||
private static string Clean(string? value) => (value ?? string.Empty).Trim();
|
||||
|
||||
private static string? MetadataLine(string label, string? value)
|
||||
@ -1010,6 +1133,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
IReadOnlyDictionary<string, int> ThreadTypeIds,
|
||||
IReadOnlyDictionary<string, int> ThreadStatusIds,
|
||||
IReadOnlyDictionary<string, int> ThreadEventTypeIds,
|
||||
IReadOnlyDictionary<string, int> SceneDependencyTypeIds,
|
||||
IReadOnlyDictionary<string, int> AssetKindIds,
|
||||
IReadOnlyDictionary<string, int> AssetStateIds,
|
||||
IReadOnlyDictionary<string, int> AssetEventTypeIds,
|
||||
|
||||
@ -11,6 +11,7 @@ public sealed class PlotLineImportPackage
|
||||
public List<ImportLocationDto> Locations { get; set; } = [];
|
||||
public List<ImportPlotLineDto> PlotLines { get; set; } = [];
|
||||
public List<ImportStoryAssetDto> StoryAssets { get; set; } = [];
|
||||
public List<ImportSceneDependencyDto> SceneDependencies { get; set; } = [];
|
||||
public List<ImportBookDto> Books { get; set; } = [];
|
||||
|
||||
[JsonExtensionData]
|
||||
@ -132,6 +133,24 @@ public sealed class ImportAssetEventDto
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ImportSceneDependencyDto
|
||||
{
|
||||
public ImportSceneReferenceDto? FromScene { get; set; }
|
||||
public ImportSceneReferenceDto? ToScene { get; set; }
|
||||
public string? DependencyType { get; set; }
|
||||
public string? Strength { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ImportSceneReferenceDto
|
||||
{
|
||||
public string? SceneRef { get; set; }
|
||||
public int? BookOrder { get; set; }
|
||||
public int? ChapterOrder { get; set; }
|
||||
public int? SceneOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ImportChapterDto
|
||||
{
|
||||
public decimal ChapterNumber { get; set; }
|
||||
@ -186,6 +205,7 @@ public sealed class ImportPreview
|
||||
public IReadOnlyList<string> UnresolvedAssetReferences { get; init; } = [];
|
||||
public IReadOnlyList<string> UnmatchedAssetOwnerNames { get; init; } = [];
|
||||
public IReadOnlyList<string> UnmatchedAssetLocationNames { get; init; } = [];
|
||||
public IReadOnlyList<string> UnresolvedSceneDependencyReferences { get; init; } = [];
|
||||
public int BookCount => Package?.Books.Count ?? 0;
|
||||
public int CharacterCount => Package?.Characters.Count ?? 0;
|
||||
public int LocationCount => Package?.Locations.Count ?? 0;
|
||||
@ -200,6 +220,8 @@ public sealed class ImportPreview
|
||||
public int ResolvedAssetStateCount => Math.Max(0, AssetStateCount - UnresolvedAssetReferences.Count(x => x.Contains("state", StringComparison.OrdinalIgnoreCase)));
|
||||
public int ResolvedAssetEventCount => Math.Max(0, AssetEventCount - UnresolvedAssetReferences.Count(x => x.Contains("event", StringComparison.OrdinalIgnoreCase)));
|
||||
public int ResolvedAssetCustodyCount => Math.Max(0, AssetCustodyCount - Package?.StoryAssets.Count(asset => !string.IsNullOrWhiteSpace(asset.InitialOwnerCharacterName) && UnmatchedAssetOwnerNames.Contains(asset.InitialOwnerCharacterName.Trim(), StringComparer.OrdinalIgnoreCase)) ?? 0);
|
||||
public int SceneDependencyCount => Package?.SceneDependencies.Count ?? 0;
|
||||
public int ResolvedSceneDependencyCount => Math.Max(0, SceneDependencyCount - UnresolvedSceneDependencyReferences.Count);
|
||||
public int ChapterCount => Package?.Books.Sum(book => book.Chapters.Count) ?? 0;
|
||||
public int SceneCount => Package?.Books.Sum(book => book.Chapters.Sum(chapter => chapter.Scenes.Count)) ?? 0;
|
||||
public int SceneCharacterAppearanceCount => Package?.Books.Sum(book => book.Chapters.Sum(chapter => chapter.Scenes.Sum(scene => scene.SceneCharacters.Count + AutoPovAppearanceCount(scene)))) ?? 0;
|
||||
@ -243,6 +265,7 @@ public sealed class ImportResult
|
||||
public int AssetStatesCreated { get; init; }
|
||||
public int AssetEventsCreated { get; init; }
|
||||
public int AssetCustodyEventsCreated { get; init; }
|
||||
public int SceneDependenciesCreated { get; init; }
|
||||
public IReadOnlyList<string> SkippedOptionalFields { get; init; } = [];
|
||||
public IReadOnlyList<string> UnresolvedWarnings { get; init; } = [];
|
||||
public string? TechnicalDetail { get; init; }
|
||||
|
||||
@ -112,6 +112,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
builder.AppendLine($"Asset states: {preview.AssetStateCount}");
|
||||
builder.AppendLine($"Asset events: {preview.AssetEventCount}");
|
||||
builder.AppendLine($"Asset custody links: {preview.AssetCustodyCount}");
|
||||
builder.AppendLine($"Scene dependencies: {preview.SceneDependencyCount}");
|
||||
builder.AppendLine($"Books: {preview.BookCount}");
|
||||
builder.AppendLine($"Chapters: {preview.ChapterCount}");
|
||||
builder.AppendLine($"Scenes: {preview.SceneCount}");
|
||||
@ -126,6 +127,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
AppendReportSection(builder, "Unresolved asset references", preview.UnresolvedAssetReferences);
|
||||
AppendReportSection(builder, "Unmatched asset owners", preview.UnmatchedAssetOwnerNames);
|
||||
AppendReportSection(builder, "Unmatched asset locations", preview.UnmatchedAssetLocationNames);
|
||||
AppendReportSection(builder, "Unresolved scene dependency references", preview.UnresolvedSceneDependencyReferences);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
@ -184,7 +186,8 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
UnresolvedThreadEventReferences = FindUnresolvedThreadEventReferences(package),
|
||||
UnresolvedAssetReferences = FindUnresolvedAssetReferences(package),
|
||||
UnmatchedAssetOwnerNames = FindUnmatchedAssetOwnerNames(package),
|
||||
UnmatchedAssetLocationNames = FindUnmatchedAssetLocationNames(package)
|
||||
UnmatchedAssetLocationNames = FindUnmatchedAssetLocationNames(package),
|
||||
UnresolvedSceneDependencyReferences = FindUnresolvedSceneDependencyReferences(package)
|
||||
};
|
||||
}
|
||||
|
||||
@ -207,10 +210,11 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
ValidateLocations(package, result);
|
||||
ValidatePlotLines(package, result);
|
||||
ValidateStoryAssets(package, result);
|
||||
ValidateSceneDependencies(package, result);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(package.PackageVersion))
|
||||
{
|
||||
result.Warnings.Add("packageVersion is missing. Version 1.0 is expected for Phase 1D.");
|
||||
result.Warnings.Add("packageVersion is missing. Version 1.0 is expected for Phase 1F.");
|
||||
}
|
||||
else if (package.PackageVersion.Trim() != "1.0")
|
||||
{
|
||||
@ -370,9 +374,68 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
result.Warnings.Add($"Story asset location \"{name}\" does not match a top-level imported location.");
|
||||
}
|
||||
|
||||
foreach (var reference in FindUnresolvedSceneDependencyReferences(package))
|
||||
{
|
||||
result.Warnings.Add($"Scene dependency reference could not be resolved and will be skipped: {reference}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateSceneDependencies(PlotLineImportPackage package, ImportValidationResult result)
|
||||
{
|
||||
foreach (var duplicate in package.SceneDependencies
|
||||
.Where(dependency => HasSceneOrderReference(dependency.FromScene) && HasSceneOrderReference(dependency.ToScene))
|
||||
.GroupBy(SceneDependencyKey, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(group => group.Count() > 1))
|
||||
{
|
||||
result.Warnings.Add($"Duplicate scene dependency will be imported once: {duplicate.Key}.");
|
||||
}
|
||||
|
||||
for (var index = 0; index < package.SceneDependencies.Count; index++)
|
||||
{
|
||||
var dependency = package.SceneDependencies[index];
|
||||
var label = $"Scene dependency {index + 1}";
|
||||
|
||||
if (dependency.FromScene is null)
|
||||
{
|
||||
result.Warnings.Add($"{label} is missing fromScene and will be skipped.");
|
||||
}
|
||||
else if (!HasSceneOrderReference(dependency.FromScene) && string.IsNullOrWhiteSpace(dependency.FromScene.SceneRef))
|
||||
{
|
||||
result.Warnings.Add($"{label} fromScene is missing bookOrder/chapterOrder/sceneOrder.");
|
||||
}
|
||||
|
||||
if (dependency.ToScene is null)
|
||||
{
|
||||
result.Warnings.Add($"{label} is missing toScene and will be skipped.");
|
||||
}
|
||||
else if (!HasSceneOrderReference(dependency.ToScene) && string.IsNullOrWhiteSpace(dependency.ToScene.SceneRef))
|
||||
{
|
||||
result.Warnings.Add($"{label} toScene is missing bookOrder/chapterOrder/sceneOrder.");
|
||||
}
|
||||
|
||||
if (HasSceneOrderReference(dependency.FromScene) && HasSceneOrderReference(dependency.ToScene)
|
||||
&& SceneReferenceKey(dependency.FromScene!) == SceneReferenceKey(dependency.ToScene!))
|
||||
{
|
||||
result.Warnings.Add($"{label} links a scene to itself and will be skipped.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dependency.DependencyType) && dependency.DependencyType.Trim().Length > 100)
|
||||
{
|
||||
result.Warnings.Add($"{label} dependencyType is longer than 100 characters and may not match an existing dependency type.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dependency.Strength) && !IsSupportedDependencyStrength(dependency.Strength))
|
||||
{
|
||||
result.Warnings.Add($"{label} strength \"{dependency.Strength.Trim()}\" is not a recognized hard/soft value. It will be preserved in the description.");
|
||||
}
|
||||
|
||||
ValidateLargeText(result, $"{label} description", dependency.Description);
|
||||
ValidateLargeText(result, $"{label} notes", dependency.Notes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateStoryAssets(PlotLineImportPackage package, ImportValidationResult result)
|
||||
{
|
||||
foreach (var group in package.StoryAssets
|
||||
@ -776,6 +839,33 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
.ToList()!;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> FindUnresolvedSceneDependencyReferences(PlotLineImportPackage package)
|
||||
{
|
||||
var sceneKeys = package.Books
|
||||
.SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => $"{book.SeriesOrder}/{chapter.Order}/{scene.Order}")))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return package.SceneDependencies
|
||||
.SelectMany((dependency, index) => new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Label = $"Dependency {index + 1} fromScene",
|
||||
Reference = dependency.FromScene
|
||||
},
|
||||
new
|
||||
{
|
||||
Label = $"Dependency {index + 1} toScene",
|
||||
Reference = dependency.ToScene
|
||||
}
|
||||
})
|
||||
.Where(x => !HasSceneOrderReference(x.Reference) || !sceneKeys.Contains(SceneReferenceKey(x.Reference!)))
|
||||
.Select(x => $"{x.Label} -> {SceneReferenceKey(x.Reference)}")
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool HasSceneOrderReference(ImportThreadEventDto threadEvent)
|
||||
=> threadEvent.BookOrder.HasValue && threadEvent.ChapterOrder.HasValue && threadEvent.SceneOrder.HasValue;
|
||||
|
||||
@ -794,6 +884,35 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
||||
private static string AssetEventSceneKey(ImportAssetEventDto assetEvent)
|
||||
=> $"{assetEvent.BookOrder?.ToString() ?? "?"}/{assetEvent.ChapterOrder?.ToString() ?? "?"}/{assetEvent.SceneOrder?.ToString() ?? "?"}";
|
||||
|
||||
private static bool HasSceneOrderReference(ImportSceneReferenceDto? reference)
|
||||
=> reference?.BookOrder.HasValue == true && reference.ChapterOrder.HasValue && reference.SceneOrder.HasValue;
|
||||
|
||||
private static string SceneReferenceKey(ImportSceneReferenceDto? reference)
|
||||
=> reference is null
|
||||
? "(missing)"
|
||||
: $"{reference.BookOrder?.ToString() ?? "?"}/{reference.ChapterOrder?.ToString() ?? "?"}/{reference.SceneOrder?.ToString() ?? "?"}";
|
||||
|
||||
private static string SceneDependencyKey(ImportSceneDependencyDto dependency)
|
||||
=> $"{SceneReferenceKey(dependency.FromScene)} -> {SceneReferenceKey(dependency.ToScene)} / {CleanDependencyType(dependency.DependencyType)}";
|
||||
|
||||
private static string CleanDependencyType(string? dependencyType)
|
||||
=> string.IsNullOrWhiteSpace(dependencyType) ? "Must Occur Before" : dependencyType.Trim();
|
||||
|
||||
private static bool IsSupportedDependencyStrength(string strength)
|
||||
{
|
||||
var normalized = strength.Trim();
|
||||
return normalized.Equals("hard", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("soft", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("strong", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("weak", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("required", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("optional", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("high", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("medium", StringComparison.OrdinalIgnoreCase)
|
||||
|| normalized.Equals("low", StringComparison.OrdinalIgnoreCase)
|
||||
|| (int.TryParse(normalized, out var numeric) && numeric is >= 1 and <= 10);
|
||||
}
|
||||
|
||||
private static HashSet<string> CharacterNames(PlotLineImportPackage package)
|
||||
=> package.Characters
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
|
||||
|
||||
@ -21,7 +21,7 @@ public sealed class ImportIndexViewModel
|
||||
public static string SampleJson => """
|
||||
{
|
||||
"packageVersion": "1.0",
|
||||
"source": "JSON Import Phase 1E sample",
|
||||
"source": "JSON Import Phase 1F sample",
|
||||
"project": {
|
||||
"title": "The Glass Meridian",
|
||||
"description": "A placeholder two-book fantasy outline.",
|
||||
@ -148,6 +148,44 @@ public sealed class ImportIndexViewModel
|
||||
]
|
||||
}
|
||||
],
|
||||
"sceneDependencies": [
|
||||
{
|
||||
"fromScene": { "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 1 },
|
||||
"toScene": { "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 2 },
|
||||
"dependencyType": "Requires",
|
||||
"strength": "Hard",
|
||||
"description": "Mara needs to find the arcade door before choosing to descend.",
|
||||
"notes": "Opening cause and effect."
|
||||
},
|
||||
{
|
||||
"fromScene": { "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 2 },
|
||||
"toScene": { "bookOrder": 1, "chapterOrder": 2, "sceneOrder": 1 },
|
||||
"dependencyType": "Reveals",
|
||||
"strength": "Strong",
|
||||
"description": "The descent sets up the first view of Lantern Court."
|
||||
},
|
||||
{
|
||||
"fromScene": { "bookOrder": 1, "chapterOrder": 2, "sceneOrder": 2 },
|
||||
"toScene": { "bookOrder": 2, "chapterOrder": 1, "sceneOrder": 1 },
|
||||
"dependencyType": "Follows",
|
||||
"strength": "Medium",
|
||||
"description": "Ilen's bargain motivates the later departure."
|
||||
},
|
||||
{
|
||||
"fromScene": { "bookOrder": 2, "chapterOrder": 1, "sceneOrder": 2 },
|
||||
"toScene": { "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 1 },
|
||||
"dependencyType": "Reveals",
|
||||
"strength": "Hard",
|
||||
"description": "The damaged signal plate must be recovered before Mara can translate it."
|
||||
},
|
||||
{
|
||||
"fromScene": { "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 1 },
|
||||
"toScene": { "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 2 },
|
||||
"dependencyType": "Blocks",
|
||||
"strength": "7",
|
||||
"description": "The warning turns the causeway crossing into a race."
|
||||
}
|
||||
],
|
||||
"books": [
|
||||
{
|
||||
"title": "Book One",
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<div>
|
||||
<p class="eyebrow">Tools</p>
|
||||
<h1>JSON Import</h1>
|
||||
<p class="lead-text">Paste or upload a Phase 1E package, preview it, confirm the summary, then import a new project.</p>
|
||||
<p class="lead-text">Paste or upload a Phase 1F package, preview it, confirm the summary, then import a new project.</p>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-primary" asp-action="Sample">Download sample JSON</a>
|
||||
@ -65,11 +65,15 @@
|
||||
</div>
|
||||
<div>
|
||||
<h3>Optional</h3>
|
||||
<p><code>packageVersion</code>, <code>source</code>, top-level <code>characters</code>, top-level <code>locations</code>, top-level <code>plotLines</code>, top-level <code>storyAssets</code>, plot line <code>threads</code>, thread <code>events</code>, asset <code>states</code>, asset <code>events</code>, initial asset owner/location mapping, POV/location mapping, date/time text, purpose/outcome text, and <code>sceneCharacters</code>.</p>
|
||||
<p><code>packageVersion</code>, <code>source</code>, top-level <code>characters</code>, top-level <code>locations</code>, top-level <code>plotLines</code>, top-level <code>storyAssets</code>, top-level <code>sceneDependencies</code>, plot line <code>threads</code>, thread <code>events</code>, asset <code>states</code>, asset <code>events</code>, initial asset owner/location mapping, POV/location mapping, date/time text, purpose/outcome text, and <code>sceneCharacters</code>.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Scene links</h3>
|
||||
<p><code>sceneDependencies</code> use <code>fromScene</code> and <code>toScene</code> objects. Link scenes with <code>bookOrder</code>, <code>chapterOrder</code>, and <code>sceneOrder</code>; <code>sceneRef</code> is optional future-friendly text. <code>dependencyType</code>, <code>strength</code>, <code>description</code>, and <code>notes</code> are optional. Warnings can be acknowledged; errors block import. Unknown fields are ignored safely.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Not imported yet</h3>
|
||||
<p>No AI extraction, Word/text parsing, manuscript parsing, scene dependency import, relationship graphs, merge/update, or export logic. Thread events and asset states/events link to scenes with <code>bookOrder</code>, <code>chapterOrder</code>, and <code>sceneOrder</code>; <code>sceneRef</code> is accepted as future-friendly text only.</p>
|
||||
<p>No AI extraction, Word/text parsing, manuscript parsing, metrics import, continuity warning import, relationship graphs, merge/update, or export logic.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@ -97,6 +101,7 @@
|
||||
<span><strong>@preview.ResolvedAssetStateCount</strong> asset states</span>
|
||||
<span><strong>@preview.ResolvedAssetEventCount</strong> asset events</span>
|
||||
<span><strong>@preview.ResolvedAssetCustodyCount</strong> custody links</span>
|
||||
<span><strong>@preview.ResolvedSceneDependencyCount</strong> dependencies</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -135,7 +140,7 @@
|
||||
|
||||
@if (preview.Package is not null)
|
||||
{
|
||||
@if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any() || preview.UnresolvedAssetReferences.Any() || preview.UnmatchedAssetOwnerNames.Any() || preview.UnmatchedAssetLocationNames.Any())
|
||||
@if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any() || preview.UnresolvedAssetReferences.Any() || preview.UnmatchedAssetOwnerNames.Any() || preview.UnmatchedAssetLocationNames.Any() || preview.UnresolvedSceneDependencyReferences.Any())
|
||||
{
|
||||
<div class="import-mapping-summary">
|
||||
@if (preview.UnmatchedPovNames.Any())
|
||||
@ -166,6 +171,10 @@
|
||||
{
|
||||
<span>Unmatched asset locations: @string.Join(", ", preview.UnmatchedAssetLocationNames)</span>
|
||||
}
|
||||
@if (preview.UnresolvedSceneDependencyReferences.Any())
|
||||
{
|
||||
<span>Unresolved dependencies: @string.Join(", ", preview.UnresolvedSceneDependencyReferences)</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -229,6 +238,37 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (preview.Package.SceneDependencies.Any())
|
||||
{
|
||||
<div class="import-tree mt-3">
|
||||
@foreach (var group in preview.Package.SceneDependencies.Select((dependency, index) => new { Dependency = dependency, Index = index + 1 }).GroupBy(x => $"{x.Dependency.FromScene?.BookOrder?.ToString() ?? "?"}/{x.Dependency.FromScene?.ChapterOrder?.ToString() ?? "?"}").OrderBy(x => x.Key))
|
||||
{
|
||||
<details class="import-tree-book">
|
||||
<summary>
|
||||
<strong>Dependencies from @group.Key</strong>
|
||||
<span>@group.Count() links</span>
|
||||
</summary>
|
||||
<ul>
|
||||
@foreach (var item in group.Take(12))
|
||||
{
|
||||
var dependency = item.Dependency;
|
||||
var fromScene = $"{dependency.FromScene?.BookOrder?.ToString() ?? "?"}/{dependency.FromScene?.ChapterOrder?.ToString() ?? "?"}/{dependency.FromScene?.SceneOrder?.ToString() ?? "?"}";
|
||||
var toScene = $"{dependency.ToScene?.BookOrder?.ToString() ?? "?"}/{dependency.ToScene?.ChapterOrder?.ToString() ?? "?"}/{dependency.ToScene?.SceneOrder?.ToString() ?? "?"}";
|
||||
<li>
|
||||
<strong>@fromScene -> @toScene</strong>
|
||||
<span class="import-scene-meta">@((dependency.DependencyType ?? "Dependency")) / @(dependency.Strength ?? "soft")</span>
|
||||
</li>
|
||||
}
|
||||
@if (group.Count() > 12)
|
||||
{
|
||||
<li class="muted">Showing first 12 links in this group.</li>
|
||||
}
|
||||
</ul>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (preview.Package.StoryAssets.Any())
|
||||
{
|
||||
<div class="import-tree mt-3">
|
||||
@ -249,7 +289,7 @@
|
||||
|
||||
<section class="import-confirmation mt-3">
|
||||
<h3>Confirm import</h3>
|
||||
<p>This will create <strong>@preview.Package.Project.Title</strong> with <strong>@preview.BookCount</strong> books, <strong>@preview.ChapterCount</strong> chapters, <strong>@preview.SceneCount</strong> scenes, <strong>@preview.CharacterCount</strong> characters, <strong>@preview.LocationCount</strong> locations, <strong>@preview.SceneCharacterAppearanceCount</strong> scene character appearances, <strong>@preview.PlotLineCount</strong> plot lines, <strong>@preview.PlotThreadCount</strong> plot threads, <strong>@preview.ResolvedThreadEventCount</strong> resolved thread events, <strong>@preview.StoryAssetCount</strong> story assets, <strong>@preview.ResolvedAssetStateCount</strong> resolved asset states, <strong>@preview.ResolvedAssetEventCount</strong> resolved asset events, and <strong>@preview.ResolvedAssetCustodyCount</strong> custody links.</p>
|
||||
<p>This will create <strong>@preview.Package.Project.Title</strong> with <strong>@preview.BookCount</strong> books, <strong>@preview.ChapterCount</strong> chapters, <strong>@preview.SceneCount</strong> scenes, <strong>@preview.CharacterCount</strong> characters, <strong>@preview.LocationCount</strong> locations, <strong>@preview.SceneCharacterAppearanceCount</strong> scene character appearances, <strong>@preview.PlotLineCount</strong> plot lines, <strong>@preview.PlotThreadCount</strong> plot threads, <strong>@preview.ResolvedThreadEventCount</strong> resolved thread events, <strong>@preview.StoryAssetCount</strong> story assets, <strong>@preview.ResolvedAssetStateCount</strong> resolved asset states, <strong>@preview.ResolvedAssetEventCount</strong> resolved asset events, <strong>@preview.ResolvedAssetCustodyCount</strong> custody links, and <strong>@preview.ResolvedSceneDependencyCount</strong> scene dependencies.</p>
|
||||
@if (preview.HasDuplicateProjectTitle)
|
||||
{
|
||||
<p class="muted">Because this title already exists, the created project will be named <strong>@preview.DuplicateImportProjectTitle</strong> if you continue.</p>
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
<span><strong>@Model.AssetStatesCreated</strong> asset states created</span>
|
||||
<span><strong>@Model.AssetEventsCreated</strong> asset events created</span>
|
||||
<span><strong>@Model.AssetCustodyEventsCreated</strong> custody records created</span>
|
||||
<span><strong>@Model.SceneDependenciesCreated</strong> scene dependencies created</span>
|
||||
</div>
|
||||
|
||||
@if (Model.SkippedOptionalFields.Any())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user