Fix story intelligence module completion lifecycle
This commit is contained in:
parent
0839b8e13d
commit
0c3f215574
@ -284,6 +284,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Story Intelligence completed Character review remains inspectable", StoryIntelligenceCompletedCharacterReviewRemainsInspectable),
|
("Story Intelligence completed Character review remains inspectable", StoryIntelligenceCompletedCharacterReviewRemainsInspectable),
|
||||||
("Story Intelligence Review Centre completed modules use canonical destinations", StoryIntelligenceReviewCentreCompletedModulesUseCanonicalDestinations),
|
("Story Intelligence Review Centre completed modules use canonical destinations", StoryIntelligenceReviewCentreCompletedModulesUseCanonicalDestinations),
|
||||||
("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages),
|
("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages),
|
||||||
|
("Story Intelligence module completion lifecycle uses persisted candidates", StoryIntelligenceModuleCompletionLifecycleUsesPersistedCandidates),
|
||||||
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution),
|
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution),
|
||||||
("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals),
|
("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals),
|
||||||
("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational),
|
("Story Intelligence recovered provider failures stay operational", StoryIntelligenceRecoveredProviderFailuresStayOperational),
|
||||||
@ -1254,6 +1255,70 @@ static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages()
|
|||||||
Assert(program.Contains("IStoryIntelligenceReviewCandidateService, StoryIntelligenceReviewCandidateService", StringComparison.Ordinal), "Persisted candidate service should be registered.");
|
Assert(program.Contains("IStoryIntelligenceReviewCandidateService, StoryIntelligenceReviewCandidateService", StringComparison.Ordinal), "Persisted candidate service should be registered.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceModuleCompletionLifecycleUsesPersistedCandidates()
|
||||||
|
{
|
||||||
|
var root = PlotLineRoot();
|
||||||
|
var candidateService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceReviewCandidateService.cs"));
|
||||||
|
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
|
||||||
|
var persistence = File.ReadAllText(Path.Combine(root, "Models/StoryIntelligencePersistenceModels.cs"));
|
||||||
|
var summarySql = File.ReadAllText(Path.Combine(root, "Sql/179_Phase27F_ReviewCentreCandidateLifecycleSummary.sql"));
|
||||||
|
var characterImport = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceCharacterImportService.cs"));
|
||||||
|
var locationImport = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceLocationImportService.cs"));
|
||||||
|
var assetImport = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceAssetImportService.cs"));
|
||||||
|
var relationshipImport = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceRelationshipImportService.cs"));
|
||||||
|
var knowledgeImport = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceKnowledgeImportService.cs"));
|
||||||
|
|
||||||
|
Assert(candidateService.Contains("Task<int> CompleteModuleIfReadyAsync(OnboardingStoryIntelligenceBatch batch, string module)", StringComparison.Ordinal), "Candidate service should expose the shared persisted completion lifecycle.");
|
||||||
|
Assert(candidateService.Contains("await candidates.CompleteGenerationAsync(generation.ReviewCandidateGenerationID)", StringComparison.Ordinal), "Successful candidate preparation should finalize the persisted generation.");
|
||||||
|
Assert(candidateService.Contains("await CompleteModuleIfReadyAsync(batch, module, completedGeneration)", StringComparison.Ordinal), "Zero-candidate generation should immediately apply the same completion rule.");
|
||||||
|
Assert(candidateService.Contains("generation.PendingCount != 0", StringComparison.Ordinal), "Non-zero and paged candidate generations must not complete early.");
|
||||||
|
Assert(candidateService.Contains("StoryIntelligenceReviewCandidateGenerationStatuses.Ready", StringComparison.Ordinal), "Failed or never-run generations must not complete.");
|
||||||
|
Assert(candidateService.Contains("RecordCharacterImportAsync", StringComparison.Ordinal)
|
||||||
|
&& candidateService.Contains("RecordLocationImportAsync", StringComparison.Ordinal)
|
||||||
|
&& candidateService.Contains("RecordAssetImportAsync", StringComparison.Ordinal)
|
||||||
|
&& candidateService.Contains("RecordRelationshipImportAsync", StringComparison.Ordinal)
|
||||||
|
&& candidateService.Contains("RecordKnowledgeImportAsync", StringComparison.Ordinal), "All candidate-review modules should share the same completion writer path.");
|
||||||
|
|
||||||
|
Assert(onboarding.Contains("var result = await characterImport.ImportAsync(batch, form, false)", StringComparison.Ordinal), "Character decisions should materialise before candidate rows are marked reviewed.");
|
||||||
|
Assert(onboarding.Contains("var pendingCount = await reviewCandidates.MarkCharacterDecisionsAsync", StringComparison.Ordinal), "Character final decision should be persisted after successful import.");
|
||||||
|
Assert(onboarding.Contains("await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Characters)", StringComparison.Ordinal), "Final Character decision should complete from module-wide pending count.");
|
||||||
|
Assert(onboarding.Contains("await wholeBookPlotIntelligence.QueueAfterCharacterResolutionAsync", StringComparison.Ordinal), "Character completion should still trigger whole-book plot synthesis.");
|
||||||
|
Assert(onboarding.Contains("var result = await locationImport.ImportAsync(batch, form, false)", StringComparison.Ordinal), "Location import should not set completion directly.");
|
||||||
|
Assert(onboarding.Contains("var result = await assetImport.ImportAsync(batch, form, false)", StringComparison.Ordinal), "Asset import should not set completion directly.");
|
||||||
|
Assert(onboarding.Contains("var result = await relationshipImport.ImportAsync(batch, form, false)", StringComparison.Ordinal), "Relationship import should not set completion directly.");
|
||||||
|
Assert(onboarding.Contains("var result = await knowledgeImport.ImportAsync(batch, form, false)", StringComparison.Ordinal), "Knowledge import should not set completion directly.");
|
||||||
|
Assert(onboarding.Contains("ReadyPendingCount(summary.LocationCandidateGenerationStatus, summary.LocationPendingCount)", StringComparison.Ordinal), "Review Centre should use persisted Location pending count instead of an empty detailed page.");
|
||||||
|
Assert(onboarding.Contains("ReadyPendingCount(summary.AssetCandidateGenerationStatus, summary.AssetPendingCount)", StringComparison.Ordinal), "Review Centre should use persisted Asset pending count instead of an empty detailed page.");
|
||||||
|
Assert(onboarding.Contains("ReadyPendingCount(summary.RelationshipCandidateGenerationStatus, summary.RelationshipPendingCount)", StringComparison.Ordinal), "Review Centre should use persisted Relationship pending count instead of an empty detailed page.");
|
||||||
|
Assert(onboarding.Contains("ReadyPendingCount(summary.KnowledgeCandidateGenerationStatus, summary.KnowledgePendingCount)", StringComparison.Ordinal), "Review Centre should use persisted Knowledge pending count instead of an empty detailed page.");
|
||||||
|
Assert(!ExtractBetween(onboarding, "private async Task<StoryIntelligenceReviewFrame?> GetReviewFrameAsync", "private sealed record StoryIntelligenceReviewFrame").Contains("CompleteModuleIfReadyAsync", StringComparison.Ordinal), "Review Centre GET must not mark module completion.");
|
||||||
|
|
||||||
|
Assert(summarySql.Contains("CandidateSummary", StringComparison.Ordinal), "Review Centre summary SQL should include persisted current generation state.");
|
||||||
|
Assert(summarySql.Contains("generation.IsCurrent = 1", StringComparison.Ordinal), "Review Centre should read only current persisted candidate generations.");
|
||||||
|
Assert(summarySql.Contains("LocationPendingCount", StringComparison.Ordinal)
|
||||||
|
&& summarySql.Contains("AssetPendingCount", StringComparison.Ordinal)
|
||||||
|
&& summarySql.Contains("RelationshipPendingCount", StringComparison.Ordinal)
|
||||||
|
&& summarySql.Contains("KnowledgePendingCount", StringComparison.Ordinal), "Summary should expose module-wide pending counts.");
|
||||||
|
Assert(!summarySql.Contains("SceneIntelligenceJson", StringComparison.Ordinal)
|
||||||
|
&& !summarySql.Contains("SceneManuscriptSources", StringComparison.Ordinal), "Review Centre summary must not reconstruct candidates from scene JSON.");
|
||||||
|
Assert(persistence.Contains("public string? LocationCandidateGenerationStatus", StringComparison.Ordinal)
|
||||||
|
&& persistence.Contains("public int? KnowledgePendingCount", StringComparison.Ordinal), "Persisted summary model should carry generation status and pending counts.");
|
||||||
|
|
||||||
|
foreach (var importService in new[] { characterImport, locationImport, assetImport, relationshipImport, knowledgeImport })
|
||||||
|
{
|
||||||
|
Assert(!importService.Contains("await pipelineState.RecordCharacterImportAsync", StringComparison.Ordinal)
|
||||||
|
&& !importService.Contains("await pipelineState.RecordLocationImportAsync", StringComparison.Ordinal)
|
||||||
|
&& !importService.Contains("await pipelineState.RecordAssetImportAsync", StringComparison.Ordinal)
|
||||||
|
&& !importService.Contains("await pipelineState.RecordRelationshipImportAsync", StringComparison.Ordinal)
|
||||||
|
&& !importService.Contains("await pipelineState.RecordKnowledgeImportAsync", StringComparison.Ordinal), "Module import services should not own durable completion writes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert(onboarding.Contains("batch.LocationStageComplete = state.LocationReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should preserve completed Location state from durable timestamps.");
|
||||||
|
Assert(onboarding.Contains("batch.RelationshipStageComplete = state.RelationshipReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should preserve completed Relationship state from durable timestamps.");
|
||||||
|
Assert(onboarding.Contains("CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Knowledge)", StringComparison.Ordinal)
|
||||||
|
&& !ExtractBetween(onboarding, "public async Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> ImportKnowledgeAsync", "public async Task<StoryIntelligenceKnowledgeImportResultViewModel?> GetKnowledgeImportResultAsync").Contains("StoryIntelligenceReviewModules.Relationships", StringComparison.Ordinal), "One module's final decision must not complete or reopen another module.");
|
||||||
|
}
|
||||||
|
|
||||||
static string ExtractAction(string value, string signature)
|
static string ExtractAction(string value, string signature)
|
||||||
{
|
{
|
||||||
var start = value.IndexOf(signature, StringComparison.Ordinal);
|
var start = value.IndexOf(signature, StringComparison.Ordinal);
|
||||||
|
|||||||
@ -164,6 +164,21 @@ public sealed class StoryIntelligenceReviewCentreSummary
|
|||||||
public DateTime? RelationshipReviewCompletedUtc { get; init; }
|
public DateTime? RelationshipReviewCompletedUtc { get; init; }
|
||||||
public DateTime? KnowledgeReviewCompletedUtc { get; init; }
|
public DateTime? KnowledgeReviewCompletedUtc { get; init; }
|
||||||
public DateTime? NarrativeReviewCompletedUtc { get; init; }
|
public DateTime? NarrativeReviewCompletedUtc { get; init; }
|
||||||
|
public string? CharacterCandidateGenerationStatus { get; init; }
|
||||||
|
public int? CharacterCandidateCount { get; init; }
|
||||||
|
public int? CharacterPendingCount { get; init; }
|
||||||
|
public string? LocationCandidateGenerationStatus { get; init; }
|
||||||
|
public int? LocationCandidateCount { get; init; }
|
||||||
|
public int? LocationPendingCount { get; init; }
|
||||||
|
public string? AssetCandidateGenerationStatus { get; init; }
|
||||||
|
public int? AssetCandidateCount { get; init; }
|
||||||
|
public int? AssetPendingCount { get; init; }
|
||||||
|
public string? RelationshipCandidateGenerationStatus { get; init; }
|
||||||
|
public int? RelationshipCandidateCount { get; init; }
|
||||||
|
public int? RelationshipPendingCount { get; init; }
|
||||||
|
public string? KnowledgeCandidateGenerationStatus { get; init; }
|
||||||
|
public int? KnowledgeCandidateCount { get; init; }
|
||||||
|
public int? KnowledgePendingCount { get; init; }
|
||||||
public int? LastRunID { get; init; }
|
public int? LastRunID { get; init; }
|
||||||
public int RunCount { get; init; }
|
public int RunCount { get; init; }
|
||||||
public int CompletedRunCount { get; init; }
|
public int CompletedRunCount { get; init; }
|
||||||
|
|||||||
@ -852,35 +852,35 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
HasCommittedScenes = hasCommittedScenes,
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
CanImport = !characterComplete,
|
CanImport = !characterComplete,
|
||||||
IsComplete = characterComplete,
|
IsComplete = characterComplete,
|
||||||
SummaryNeedsAttentionCount = characterComplete ? 0 : null
|
SummaryNeedsAttentionCount = characterComplete ? 0 : ReadyPendingCount(summary.CharacterCandidateGenerationStatus, summary.CharacterPendingCount)
|
||||||
},
|
},
|
||||||
LocationReview = new StoryIntelligenceLocationReviewViewModel
|
LocationReview = new StoryIntelligenceLocationReviewViewModel
|
||||||
{
|
{
|
||||||
HasCommittedScenes = hasCommittedScenes,
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
CanImport = !locationComplete,
|
CanImport = !locationComplete,
|
||||||
IsComplete = locationComplete,
|
IsComplete = locationComplete,
|
||||||
SummaryNeedsAttentionCount = locationComplete ? 0 : null
|
SummaryNeedsAttentionCount = locationComplete ? 0 : ReadyPendingCount(summary.LocationCandidateGenerationStatus, summary.LocationPendingCount)
|
||||||
},
|
},
|
||||||
AssetReview = new StoryIntelligenceAssetReviewViewModel
|
AssetReview = new StoryIntelligenceAssetReviewViewModel
|
||||||
{
|
{
|
||||||
HasCommittedScenes = hasCommittedScenes,
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
CanImport = !assetComplete,
|
CanImport = !assetComplete,
|
||||||
IsComplete = assetComplete,
|
IsComplete = assetComplete,
|
||||||
SummaryNeedsAttentionCount = assetComplete ? 0 : null
|
SummaryNeedsAttentionCount = assetComplete ? 0 : ReadyPendingCount(summary.AssetCandidateGenerationStatus, summary.AssetPendingCount)
|
||||||
},
|
},
|
||||||
RelationshipReview = new StoryIntelligenceRelationshipReviewViewModel
|
RelationshipReview = new StoryIntelligenceRelationshipReviewViewModel
|
||||||
{
|
{
|
||||||
HasCommittedScenes = hasCommittedScenes,
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
CanImport = !relationshipComplete,
|
CanImport = !relationshipComplete,
|
||||||
IsComplete = relationshipComplete,
|
IsComplete = relationshipComplete,
|
||||||
SummaryNeedsAttentionCount = relationshipComplete ? 0 : null
|
SummaryNeedsAttentionCount = relationshipComplete ? 0 : ReadyPendingCount(summary.RelationshipCandidateGenerationStatus, summary.RelationshipPendingCount)
|
||||||
},
|
},
|
||||||
KnowledgeReview = new StoryIntelligenceKnowledgeReviewViewModel
|
KnowledgeReview = new StoryIntelligenceKnowledgeReviewViewModel
|
||||||
{
|
{
|
||||||
HasCommittedScenes = hasCommittedScenes,
|
HasCommittedScenes = hasCommittedScenes,
|
||||||
CanImport = !knowledgeComplete,
|
CanImport = !knowledgeComplete,
|
||||||
IsComplete = knowledgeComplete,
|
IsComplete = knowledgeComplete,
|
||||||
SummaryNeedsAttentionCount = knowledgeComplete ? 0 : null
|
SummaryNeedsAttentionCount = knowledgeComplete ? 0 : ReadyPendingCount(summary.KnowledgeCandidateGenerationStatus, summary.KnowledgePendingCount)
|
||||||
},
|
},
|
||||||
NarrativeReview = new StoryIntelligenceNarrativeReviewViewModel
|
NarrativeReview = new StoryIntelligenceNarrativeReviewViewModel
|
||||||
{
|
{
|
||||||
@ -916,6 +916,11 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return new StoryIntelligenceReviewFrame(batch, summary, model, summaryStopwatch.ElapsedMilliseconds);
|
return new StoryIntelligenceReviewFrame(batch, summary, model, summaryStopwatch.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int? ReadyPendingCount(string? generationStatus, int? pendingCount)
|
||||||
|
=> string.Equals(generationStatus, StoryIntelligenceReviewCandidateGenerationStatuses.Ready, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? pendingCount ?? 0
|
||||||
|
: null;
|
||||||
|
|
||||||
private sealed record StoryIntelligenceReviewFrame(
|
private sealed record StoryIntelligenceReviewFrame(
|
||||||
OnboardingStoryIntelligenceBatch Batch,
|
OnboardingStoryIntelligenceBatch Batch,
|
||||||
StoryIntelligenceReviewCentreSummary Summary,
|
StoryIntelligenceReviewCentreSummary Summary,
|
||||||
@ -984,6 +989,12 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
|
|
||||||
await onboarding.MarkCompletedAsync();
|
await onboarding.MarkCompletedAsync();
|
||||||
await reviewCandidates.PrepareCharacterCandidatesAsync(batch);
|
await reviewCandidates.PrepareCharacterCandidatesAsync(batch);
|
||||||
|
var pendingCharacterCandidates = await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Characters);
|
||||||
|
if (pendingCharacterCandidates == 0)
|
||||||
|
{
|
||||||
|
await wholeBookPlotIntelligence.QueueAfterCharacterResolutionAsync(batch.ProjectID, batch.BookID, batch.UserID);
|
||||||
|
await reviewCandidates.PreparePeerCandidatesAsync(batch);
|
||||||
|
}
|
||||||
return (await GetProgressAsync(batchId), new StoryIntelligenceImportCommitResult
|
return (await GetProgressAsync(batchId), new StoryIntelligenceImportCommitResult
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
@ -1000,12 +1011,14 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var pendingCount = await reviewCandidates.MarkCharacterDecisionsAsync(batch.BookID, form.Characters);
|
var result = await characterImport.ImportAsync(batch, form, false);
|
||||||
var result = await characterImport.ImportAsync(batch, form, pendingCount == 0);
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
|
var pendingCount = await reviewCandidates.MarkCharacterDecisionsAsync(batch.BookID, form.Characters);
|
||||||
if (pendingCount == 0)
|
if (pendingCount == 0)
|
||||||
{
|
{
|
||||||
|
await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Characters);
|
||||||
|
await wholeBookPlotIntelligence.QueueAfterCharacterResolutionAsync(batch.ProjectID, batch.BookID, batch.UserID);
|
||||||
await reviewCandidates.PreparePeerCandidatesAsync(batch);
|
await reviewCandidates.PreparePeerCandidatesAsync(batch);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -1046,11 +1059,18 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var pendingCount = await reviewCandidates.MarkLocationDecisionsAsync(batch.BookID, form.Locations);
|
var result = await locationImport.ImportAsync(batch, form, false);
|
||||||
var result = await locationImport.ImportAsync(batch, form, pendingCount == 0);
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
result = pendingCount == 0 ? result : WithRemainingMessage(result, pendingCount, "location");
|
var pendingCount = await reviewCandidates.MarkLocationDecisionsAsync(batch.BookID, form.Locations);
|
||||||
|
if (pendingCount == 0)
|
||||||
|
{
|
||||||
|
await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Locations);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = WithRemainingMessage(result, pendingCount, "location");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (await GetReviewCentreAsync(batchId), result);
|
return (await GetReviewCentreAsync(batchId), result);
|
||||||
}
|
}
|
||||||
@ -1084,11 +1104,18 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var pendingCount = await reviewCandidates.MarkAssetDecisionsAsync(batch.BookID, form.Assets);
|
var result = await assetImport.ImportAsync(batch, form, false);
|
||||||
var result = await assetImport.ImportAsync(batch, form, pendingCount == 0);
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
result = pendingCount == 0 ? result : WithRemainingMessage(result, pendingCount, "asset");
|
var pendingCount = await reviewCandidates.MarkAssetDecisionsAsync(batch.BookID, form.Assets);
|
||||||
|
if (pendingCount == 0)
|
||||||
|
{
|
||||||
|
await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Assets);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = WithRemainingMessage(result, pendingCount, "asset");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (await GetReviewCentreAsync(batchId), result);
|
return (await GetReviewCentreAsync(batchId), result);
|
||||||
}
|
}
|
||||||
@ -1123,11 +1150,18 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var pendingCount = await reviewCandidates.MarkRelationshipDecisionsAsync(batch.BookID, form.Relationships);
|
var result = await relationshipImport.ImportAsync(batch, form, false);
|
||||||
var result = await relationshipImport.ImportAsync(batch, form, pendingCount == 0);
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
result = pendingCount == 0 ? result : WithRemainingMessage(result, pendingCount, "relationship");
|
var pendingCount = await reviewCandidates.MarkRelationshipDecisionsAsync(batch.BookID, form.Relationships);
|
||||||
|
if (pendingCount == 0)
|
||||||
|
{
|
||||||
|
await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Relationships);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = WithRemainingMessage(result, pendingCount, "relationship");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (await GetReviewCentreAsync(batchId), result);
|
return (await GetReviewCentreAsync(batchId), result);
|
||||||
}
|
}
|
||||||
@ -1161,11 +1195,18 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
return (null, new StoryIntelligenceImportCommitResult { Success = false, Message = "This Story Intelligence batch could not be found." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var pendingCount = await reviewCandidates.MarkKnowledgeDecisionsAsync(batch.BookID, form.Knowledge);
|
var result = await knowledgeImport.ImportAsync(batch, form, false);
|
||||||
var result = await knowledgeImport.ImportAsync(batch, form, pendingCount == 0);
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
result = pendingCount == 0 ? result : WithRemainingMessage(result, pendingCount, "knowledge");
|
var pendingCount = await reviewCandidates.MarkKnowledgeDecisionsAsync(batch.BookID, form.Knowledge);
|
||||||
|
if (pendingCount == 0)
|
||||||
|
{
|
||||||
|
await reviewCandidates.CompleteModuleIfReadyAsync(batch, StoryIntelligenceReviewModules.Knowledge);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = WithRemainingMessage(result, pendingCount, "knowledge");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (await GetReviewCentreAsync(batchId), result);
|
return (await GetReviewCentreAsync(batchId), result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,10 +111,6 @@ public sealed class StoryIntelligenceAssetImportService(
|
|||||||
{
|
{
|
||||||
batch.AssetStageComplete = completeReview;
|
batch.AssetStageComplete = completeReview;
|
||||||
batch.LastAssetImportResult = new StoryIntelligenceAssetImportBatchResult();
|
batch.LastAssetImportResult = new StoryIntelligenceAssetImportBatchResult();
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordAssetImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
return new StoryIntelligenceImportCommitResult
|
return new StoryIntelligenceImportCommitResult
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
@ -302,11 +298,6 @@ public sealed class StoryIntelligenceAssetImportService(
|
|||||||
SceneAssetEventsCreated = sceneEventsCreated,
|
SceneAssetEventsCreated = sceneEventsCreated,
|
||||||
OwnershipLinksCreated = ownershipLinksCreated
|
OwnershipLinksCreated = ownershipLinksCreated
|
||||||
};
|
};
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordAssetImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Imported Story Intelligence assets for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Aliases={Aliases} Ignored={Ignored} SceneEvents={SceneEvents} Ownership={Ownership}",
|
"Imported Story Intelligence assets for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Aliases={Aliases} Ignored={Ignored} SceneEvents={SceneEvents} Ownership={Ownership}",
|
||||||
batch.BatchID,
|
batch.BatchID,
|
||||||
|
|||||||
@ -19,7 +19,6 @@ public sealed class StoryIntelligenceCharacterImportService(
|
|||||||
IStoryIntelligencePipelineStateService pipelineState,
|
IStoryIntelligencePipelineStateService pipelineState,
|
||||||
INameIntelligenceService nameIntelligence,
|
INameIntelligenceService nameIntelligence,
|
||||||
ICoreImportCharacterMappingService coreImportCharacterMapping,
|
ICoreImportCharacterMappingService coreImportCharacterMapping,
|
||||||
IWholeBookPlotIntelligenceService wholeBookPlotIntelligence,
|
|
||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
ILogger<StoryIntelligenceCharacterImportService> logger) : IStoryIntelligenceCharacterImportService
|
ILogger<StoryIntelligenceCharacterImportService> logger) : IStoryIntelligenceCharacterImportService
|
||||||
{
|
{
|
||||||
@ -384,12 +383,6 @@ public sealed class StoryIntelligenceCharacterImportService(
|
|||||||
ScenePeoplePanelsUpdated = linkedAppearances
|
ScenePeoplePanelsUpdated = linkedAppearances
|
||||||
};
|
};
|
||||||
var remap = await coreImportCharacterMapping.MapBookAsync(batch.ProjectID, batch.BookID);
|
var remap = await coreImportCharacterMapping.MapBookAsync(batch.ProjectID, batch.BookID);
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordCharacterImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
await wholeBookPlotIntelligence.QueueAfterCharacterResolutionAsync(batch.ProjectID, batch.BookID, batch.UserID);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Imported Story Intelligence characters for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Aliased={Aliased} Ignored={Ignored} LinkedAppearances={LinkedAppearances} PovLinks={PovLinks} RemappedScenes={RemappedScenes} RemappedLinks={RemappedLinks}",
|
"Imported Story Intelligence characters for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Aliased={Aliased} Ignored={Ignored} LinkedAppearances={LinkedAppearances} PovLinks={PovLinks} RemappedScenes={RemappedScenes} RemappedLinks={RemappedLinks}",
|
||||||
batch.BatchID,
|
batch.BatchID,
|
||||||
|
|||||||
@ -104,10 +104,6 @@ public sealed class StoryIntelligenceKnowledgeImportService(
|
|||||||
KnowledgeMerged = batch.KnowledgeDecisions.Count(decision => string.Equals(decision.Action, StoryIntelligenceKnowledgeImportActions.Alias, StringComparison.OrdinalIgnoreCase)),
|
KnowledgeMerged = batch.KnowledgeDecisions.Count(decision => string.Equals(decision.Action, StoryIntelligenceKnowledgeImportActions.Alias, StringComparison.OrdinalIgnoreCase)),
|
||||||
KnowledgeIgnored = batch.KnowledgeDecisions.Count(decision => string.Equals(decision.Action, StoryIntelligenceKnowledgeImportActions.Ignore, StringComparison.OrdinalIgnoreCase))
|
KnowledgeIgnored = batch.KnowledgeDecisions.Count(decision => string.Equals(decision.Action, StoryIntelligenceKnowledgeImportActions.Ignore, StringComparison.OrdinalIgnoreCase))
|
||||||
};
|
};
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordKnowledgeImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
return new StoryIntelligenceImportCommitResult
|
return new StoryIntelligenceImportCommitResult
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
@ -215,11 +211,6 @@ public sealed class StoryIntelligenceKnowledgeImportService(
|
|||||||
KnowledgeMerged = merged,
|
KnowledgeMerged = merged,
|
||||||
KnowledgeIgnored = ignored
|
KnowledgeIgnored = ignored
|
||||||
};
|
};
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordKnowledgeImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Imported Story Intelligence knowledge for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Merged={Merged} Ignored={Ignored}",
|
"Imported Story Intelligence knowledge for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Merged={Merged} Ignored={Ignored}",
|
||||||
batch.BatchID,
|
batch.BatchID,
|
||||||
|
|||||||
@ -278,11 +278,6 @@ public sealed class StoryIntelligenceLocationImportService(
|
|||||||
LocationsIgnored = ignored,
|
LocationsIgnored = ignored,
|
||||||
SceneLocationsUpdated = sceneLocationsUpdated
|
SceneLocationsUpdated = sceneLocationsUpdated
|
||||||
};
|
};
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordLocationImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Imported Story Intelligence locations for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Aliases={Aliases} Ignored={Ignored} SceneLocationsUpdated={SceneLocationsUpdated}",
|
"Imported Story Intelligence locations for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Aliases={Aliases} Ignored={Ignored} SceneLocationsUpdated={SceneLocationsUpdated}",
|
||||||
batch.BatchID,
|
batch.BatchID,
|
||||||
|
|||||||
@ -92,10 +92,6 @@ public sealed class StoryIntelligenceRelationshipImportService(
|
|||||||
{
|
{
|
||||||
batch.RelationshipStageComplete = completeReview;
|
batch.RelationshipStageComplete = completeReview;
|
||||||
batch.LastRelationshipImportResult = new StoryIntelligenceRelationshipImportBatchResult();
|
batch.LastRelationshipImportResult = new StoryIntelligenceRelationshipImportBatchResult();
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordRelationshipImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
return new StoryIntelligenceImportCommitResult
|
return new StoryIntelligenceImportCommitResult
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
@ -246,11 +242,6 @@ public sealed class StoryIntelligenceRelationshipImportService(
|
|||||||
RelationshipsIgnored = ignored,
|
RelationshipsIgnored = ignored,
|
||||||
RelationshipEventsCreated = eventsCreated
|
RelationshipEventsCreated = eventsCreated
|
||||||
};
|
};
|
||||||
if (completeReview)
|
|
||||||
{
|
|
||||||
await pipelineState.RecordRelationshipImportAsync(batch.ProjectID, batch.BookID);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Imported Story Intelligence relationships for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Merged={Merged} Ignored={Ignored} Events={Events}",
|
"Imported Story Intelligence relationships for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Merged={Merged} Ignored={Ignored} Events={Events}",
|
||||||
batch.BatchID,
|
batch.BatchID,
|
||||||
|
|||||||
@ -20,6 +20,7 @@ public interface IStoryIntelligenceReviewCandidateService
|
|||||||
Task<int> MarkAssetDecisionsAsync(int bookId, IEnumerable<StoryIntelligenceAssetImportChoiceForm> choices);
|
Task<int> MarkAssetDecisionsAsync(int bookId, IEnumerable<StoryIntelligenceAssetImportChoiceForm> choices);
|
||||||
Task<int> MarkRelationshipDecisionsAsync(int bookId, IEnumerable<StoryIntelligenceRelationshipImportChoiceForm> choices);
|
Task<int> MarkRelationshipDecisionsAsync(int bookId, IEnumerable<StoryIntelligenceRelationshipImportChoiceForm> choices);
|
||||||
Task<int> MarkKnowledgeDecisionsAsync(int bookId, IEnumerable<StoryIntelligenceKnowledgeImportChoiceForm> choices);
|
Task<int> MarkKnowledgeDecisionsAsync(int bookId, IEnumerable<StoryIntelligenceKnowledgeImportChoiceForm> choices);
|
||||||
|
Task<int> CompleteModuleIfReadyAsync(OnboardingStoryIntelligenceBatch batch, string module);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class StoryIntelligenceReviewCandidateService(
|
public sealed class StoryIntelligenceReviewCandidateService(
|
||||||
@ -30,6 +31,7 @@ public sealed class StoryIntelligenceReviewCandidateService(
|
|||||||
IStoryIntelligenceRelationshipImportService relationshipImport,
|
IStoryIntelligenceRelationshipImportService relationshipImport,
|
||||||
IStoryIntelligenceKnowledgeImportService knowledgeImport,
|
IStoryIntelligenceKnowledgeImportService knowledgeImport,
|
||||||
ICharacterRepository characters,
|
ICharacterRepository characters,
|
||||||
|
IStoryIntelligencePipelineStateService pipelineState,
|
||||||
ILogger<StoryIntelligenceReviewCandidateService> logger) : IStoryIntelligenceReviewCandidateService
|
ILogger<StoryIntelligenceReviewCandidateService> logger) : IStoryIntelligenceReviewCandidateService
|
||||||
{
|
{
|
||||||
private const int MaxPageSize = 100;
|
private const int MaxPageSize = 100;
|
||||||
@ -354,7 +356,8 @@ public sealed class StoryIntelligenceReviewCandidateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await candidates.CompleteGenerationAsync(generation.ReviewCandidateGenerationID);
|
var completedGeneration = await candidates.CompleteGenerationAsync(generation.ReviewCandidateGenerationID);
|
||||||
|
await CompleteModuleIfReadyAsync(batch, module, completedGeneration);
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Prepared persisted Story Intelligence review candidates. BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} Module={Module} Count={Count}",
|
"Prepared persisted Story Intelligence review candidates. BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} Module={Module} Count={Count}",
|
||||||
batch.BatchID,
|
batch.BatchID,
|
||||||
@ -371,6 +374,49 @@ public sealed class StoryIntelligenceReviewCandidateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> CompleteModuleIfReadyAsync(OnboardingStoryIntelligenceBatch batch, string module)
|
||||||
|
{
|
||||||
|
var generation = await candidates.GetCurrentGenerationAsync(batch.BookID, module);
|
||||||
|
return await CompleteModuleIfReadyAsync(batch, module, generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> CompleteModuleIfReadyAsync(
|
||||||
|
OnboardingStoryIntelligenceBatch batch,
|
||||||
|
string module,
|
||||||
|
StoryIntelligenceReviewCandidateGeneration? generation)
|
||||||
|
{
|
||||||
|
if (generation is null
|
||||||
|
|| !string.Equals(generation.Status, StoryIntelligenceReviewCandidateGenerationStatuses.Ready, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| generation.PendingCount != 0)
|
||||||
|
{
|
||||||
|
return generation?.PendingCount ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (module)
|
||||||
|
{
|
||||||
|
case StoryIntelligenceReviewModules.Characters:
|
||||||
|
await pipelineState.RecordCharacterImportAsync(batch.ProjectID, batch.BookID);
|
||||||
|
break;
|
||||||
|
case StoryIntelligenceReviewModules.Locations:
|
||||||
|
await pipelineState.RecordLocationImportAsync(batch.ProjectID, batch.BookID);
|
||||||
|
break;
|
||||||
|
case StoryIntelligenceReviewModules.Assets:
|
||||||
|
await pipelineState.RecordAssetImportAsync(batch.ProjectID, batch.BookID);
|
||||||
|
break;
|
||||||
|
case StoryIntelligenceReviewModules.Relationships:
|
||||||
|
await pipelineState.RecordRelationshipImportAsync(batch.ProjectID, batch.BookID);
|
||||||
|
break;
|
||||||
|
case StoryIntelligenceReviewModules.Knowledge:
|
||||||
|
await pipelineState.RecordKnowledgeImportAsync(batch.ProjectID, batch.BookID);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.LogWarning("Ignoring unsupported Story Intelligence review completion module {Module}. BatchID={BatchID} BookID={BookID}", module, batch.BatchID, batch.BookID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<CandidatePage<TCandidate>> LoadPageAsync<TCandidate>(int bookId, string module, int pageNumber, int pageSize)
|
private async Task<CandidatePage<TCandidate>> LoadPageAsync<TCandidate>(int bookId, string module, int pageNumber, int pageSize)
|
||||||
{
|
{
|
||||||
pageNumber = Math.Max(1, pageNumber);
|
pageNumber = Math.Max(1, pageNumber);
|
||||||
|
|||||||
@ -0,0 +1,175 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceReviewCentre_GetSummary
|
||||||
|
@BookID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
;WITH RunSummary AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
r.BookID,
|
||||||
|
COUNT_BIG(*) AS RunCount,
|
||||||
|
SUM(CASE WHEN r.Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END) AS CompletedRunCount,
|
||||||
|
SUM(CASE WHEN r.Status IN (N'Pending', N'Running') THEN 1 ELSE 0 END) AS ActiveRunCount,
|
||||||
|
SUM(CASE WHEN r.Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) AS FailedRunCount,
|
||||||
|
SUM(COALESCE(r.TotalDetectedScenes, 0)) AS TotalDetectedScenes,
|
||||||
|
SUM(COALESCE(r.CompletedScenes, 0)) AS CompletedScenes,
|
||||||
|
SUM(COALESCE(r.FailedScenes, 0)) AS FailedScenes
|
||||||
|
FROM dbo.StoryIntelligenceRuns r
|
||||||
|
WHERE r.BookID = @BookID
|
||||||
|
GROUP BY r.BookID
|
||||||
|
),
|
||||||
|
CandidateSummary AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
generation.BookID,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Characters' THEN generation.Status END) AS CharacterCandidateGenerationStatus,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Characters' THEN generation.CandidateCount END) AS CharacterCandidateCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Characters' THEN generation.PendingCount END) AS CharacterPendingCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Locations' THEN generation.Status END) AS LocationCandidateGenerationStatus,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Locations' THEN generation.CandidateCount END) AS LocationCandidateCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Locations' THEN generation.PendingCount END) AS LocationPendingCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Assets' THEN generation.Status END) AS AssetCandidateGenerationStatus,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Assets' THEN generation.CandidateCount END) AS AssetCandidateCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Assets' THEN generation.PendingCount END) AS AssetPendingCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Relationships' THEN generation.Status END) AS RelationshipCandidateGenerationStatus,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Relationships' THEN generation.CandidateCount END) AS RelationshipCandidateCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Relationships' THEN generation.PendingCount END) AS RelationshipPendingCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Knowledge' THEN generation.Status END) AS KnowledgeCandidateGenerationStatus,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Knowledge' THEN generation.CandidateCount END) AS KnowledgeCandidateCount,
|
||||||
|
MAX(CASE WHEN generation.Module = N'Knowledge' THEN generation.PendingCount END) AS KnowledgePendingCount
|
||||||
|
FROM dbo.StoryIntelligenceReviewCandidateGenerations generation
|
||||||
|
WHERE generation.BookID = @BookID
|
||||||
|
AND generation.IsCurrent = 1
|
||||||
|
GROUP BY generation.BookID
|
||||||
|
),
|
||||||
|
NarrativeSummary AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
nc.BookID,
|
||||||
|
COUNT_BIG(*) AS NarrativeCandidateCount,
|
||||||
|
SUM(CASE WHEN nc.Status = N'Pending' THEN 1 ELSE 0 END) AS NarrativePendingCount,
|
||||||
|
SUM(CASE WHEN nc.CandidateType = N'Thread' THEN 1 ELSE 0 END) AS NarrativeThreadCount
|
||||||
|
FROM dbo.StoryIntelligenceNarrativeCandidates nc
|
||||||
|
WHERE nc.BookID = @BookID
|
||||||
|
AND nc.WholeBookPlotSynthesisRunID IS NOT NULL
|
||||||
|
GROUP BY nc.BookID
|
||||||
|
),
|
||||||
|
NarrativeEventSummary AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
nc.BookID,
|
||||||
|
COUNT_BIG(*) AS NarrativeEventCount
|
||||||
|
FROM dbo.StoryIntelligenceNarrativeCandidateEvents ev
|
||||||
|
INNER JOIN dbo.StoryIntelligenceNarrativeCandidates nc
|
||||||
|
ON nc.NarrativeCandidateID = ev.NarrativeCandidateID
|
||||||
|
WHERE nc.BookID = @BookID
|
||||||
|
AND nc.WholeBookPlotSynthesisRunID IS NOT NULL
|
||||||
|
GROUP BY nc.BookID
|
||||||
|
),
|
||||||
|
MetricValueSummary AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
chapter.BookID,
|
||||||
|
COUNT_BIG(*) AS MetricValueCount
|
||||||
|
FROM dbo.SceneMetricValues metric
|
||||||
|
INNER JOIN dbo.Scenes scene
|
||||||
|
ON scene.SceneID = metric.SceneID
|
||||||
|
INNER JOIN dbo.Chapters chapter
|
||||||
|
ON chapter.ChapterID = scene.ChapterID
|
||||||
|
WHERE chapter.BookID = @BookID
|
||||||
|
GROUP BY chapter.BookID
|
||||||
|
),
|
||||||
|
CurrentPlotRun AS
|
||||||
|
(
|
||||||
|
SELECT TOP (1)
|
||||||
|
run.BookID,
|
||||||
|
run.WholeBookPlotSynthesisRunID,
|
||||||
|
run.Status AS WholeBookPlotSynthesisStatus,
|
||||||
|
run.ErrorMessage AS WholeBookPlotSynthesisErrorMessage
|
||||||
|
FROM dbo.WholeBookPlotSynthesisRuns run
|
||||||
|
WHERE run.BookID = @BookID
|
||||||
|
AND run.IsCurrent = 1
|
||||||
|
ORDER BY run.WholeBookPlotSynthesisRunID DESC
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.StoryIntelligenceBookPipelineID,
|
||||||
|
p.ProjectID,
|
||||||
|
project.ProjectName,
|
||||||
|
p.BookID,
|
||||||
|
book.BookTitle,
|
||||||
|
book.Subtitle AS BookSubtitle,
|
||||||
|
p.CurrentStage,
|
||||||
|
p.LastCompletedStage,
|
||||||
|
p.CurrentReviewStage,
|
||||||
|
p.Status,
|
||||||
|
p.CompletedUtc,
|
||||||
|
p.CharacterReviewCompletedUtc,
|
||||||
|
p.LocationReviewCompletedUtc,
|
||||||
|
p.AssetReviewCompletedUtc,
|
||||||
|
p.RelationshipReviewCompletedUtc,
|
||||||
|
p.KnowledgeReviewCompletedUtc,
|
||||||
|
p.NarrativeReviewCompletedUtc,
|
||||||
|
cs.CharacterCandidateGenerationStatus,
|
||||||
|
CAST(cs.CharacterCandidateCount AS int) AS CharacterCandidateCount,
|
||||||
|
CAST(cs.CharacterPendingCount AS int) AS CharacterPendingCount,
|
||||||
|
cs.LocationCandidateGenerationStatus,
|
||||||
|
CAST(cs.LocationCandidateCount AS int) AS LocationCandidateCount,
|
||||||
|
CAST(cs.LocationPendingCount AS int) AS LocationPendingCount,
|
||||||
|
cs.AssetCandidateGenerationStatus,
|
||||||
|
CAST(cs.AssetCandidateCount AS int) AS AssetCandidateCount,
|
||||||
|
CAST(cs.AssetPendingCount AS int) AS AssetPendingCount,
|
||||||
|
cs.RelationshipCandidateGenerationStatus,
|
||||||
|
CAST(cs.RelationshipCandidateCount AS int) AS RelationshipCandidateCount,
|
||||||
|
CAST(cs.RelationshipPendingCount AS int) AS RelationshipPendingCount,
|
||||||
|
cs.KnowledgeCandidateGenerationStatus,
|
||||||
|
CAST(cs.KnowledgeCandidateCount AS int) AS KnowledgeCandidateCount,
|
||||||
|
CAST(cs.KnowledgePendingCount AS int) AS KnowledgePendingCount,
|
||||||
|
p.LastRunID,
|
||||||
|
CAST(COALESCE(rs.RunCount, 0) AS int) AS RunCount,
|
||||||
|
CAST(COALESCE(rs.CompletedRunCount, 0) AS int) AS CompletedRunCount,
|
||||||
|
CAST(COALESCE(rs.ActiveRunCount, 0) AS int) AS ActiveRunCount,
|
||||||
|
CAST(COALESCE(rs.FailedRunCount, 0) AS int) AS FailedRunCount,
|
||||||
|
CAST(COALESCE(rs.TotalDetectedScenes, 0) AS int) AS TotalDetectedScenes,
|
||||||
|
CAST(COALESCE(rs.CompletedScenes, 0) AS int) AS CompletedScenes,
|
||||||
|
CAST(COALESCE(rs.FailedScenes, 0) AS int) AS FailedScenes,
|
||||||
|
plot.WholeBookPlotSynthesisRunID,
|
||||||
|
plot.WholeBookPlotSynthesisStatus,
|
||||||
|
plot.WholeBookPlotSynthesisErrorMessage,
|
||||||
|
CAST(COALESCE(ns.NarrativeCandidateCount, 0) AS int) AS NarrativeCandidateCount,
|
||||||
|
CAST(COALESCE(ns.NarrativePendingCount, 0) AS int) AS NarrativePendingCount,
|
||||||
|
CAST(COALESCE(ns.NarrativeThreadCount, 0) AS int) AS NarrativeThreadCount,
|
||||||
|
CAST(COALESCE(nes.NarrativeEventCount, 0) AS int) AS NarrativeEventCount,
|
||||||
|
CAST(COALESCE(metrics.MetricValueCount, 0) AS int) AS MetricValueCount
|
||||||
|
FROM dbo.StoryIntelligenceBookPipelines p
|
||||||
|
INNER JOIN dbo.Projects project
|
||||||
|
ON project.ProjectID = p.ProjectID
|
||||||
|
INNER JOIN dbo.Books book
|
||||||
|
ON book.BookID = p.BookID
|
||||||
|
INNER JOIN dbo.ProjectUserAccess access
|
||||||
|
ON access.ProjectID = p.ProjectID
|
||||||
|
AND access.UserID = @UserID
|
||||||
|
AND access.IsActive = 1
|
||||||
|
LEFT JOIN RunSummary rs
|
||||||
|
ON rs.BookID = p.BookID
|
||||||
|
LEFT JOIN CandidateSummary cs
|
||||||
|
ON cs.BookID = p.BookID
|
||||||
|
LEFT JOIN NarrativeSummary ns
|
||||||
|
ON ns.BookID = p.BookID
|
||||||
|
LEFT JOIN NarrativeEventSummary nes
|
||||||
|
ON nes.BookID = p.BookID
|
||||||
|
LEFT JOIN MetricValueSummary metrics
|
||||||
|
ON metrics.BookID = p.BookID
|
||||||
|
LEFT JOIN CurrentPlotRun plot
|
||||||
|
ON plot.BookID = p.BookID
|
||||||
|
WHERE p.BookID = @BookID
|
||||||
|
AND book.IsArchived = 0
|
||||||
|
AND project.IsArchived = 0;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
Loading…
x
Reference in New Issue
Block a user