Fix independent review centre state

This commit is contained in:
Nick Beckley 2026-08-30 19:50:15 +00:00
parent 794606f723
commit 5a7b8c79c7
14 changed files with 451 additions and 79 deletions

View File

@ -270,6 +270,7 @@ var tests = new (string Name, Action Test)[]
("Story Intelligence Review Centre uses independent tile grid", StoryIntelligenceReviewCentreUsesIndependentTileGrid), ("Story Intelligence Review Centre uses independent tile grid", StoryIntelligenceReviewCentreUsesIndependentTileGrid),
("Story Intelligence Review Centre opens with character gate", StoryIntelligenceReviewCentreOpensWithCharacterGate), ("Story Intelligence Review Centre opens with character gate", StoryIntelligenceReviewCentreOpensWithCharacterGate),
("Story Intelligence review modules are independent after character gate", StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate), ("Story Intelligence review modules are independent after character gate", StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate),
("Story Intelligence review module completion is durable and independent", StoryIntelligenceReviewModuleCompletionIsDurableAndIndependent),
("Story Intelligence review module pages omit wizard chrome", StoryIntelligenceReviewModulePagesOmitWizardChrome), ("Story Intelligence review module pages omit wizard chrome", StoryIntelligenceReviewModulePagesOmitWizardChrome),
("Story Intelligence downstream review uses completed scene analysis", StoryIntelligenceDownstreamReviewUsesCompletedSceneAnalysis), ("Story Intelligence downstream review uses completed scene analysis", StoryIntelligenceDownstreamReviewUsesCompletedSceneAnalysis),
("Story Intelligence review posts do not use success interstitials", StoryIntelligenceReviewPostsDoNotUseSuccessInterstitials), ("Story Intelligence review posts do not use success interstitials", StoryIntelligenceReviewPostsDoNotUseSuccessInterstitials),
@ -802,6 +803,77 @@ static void StoryIntelligenceReviewModulesAreIndependentAfterCharacterGate()
Assert(locationsAction.Contains("RedirectToReviewCentreWithCharacterGate", StringComparison.Ordinal), "Direct dependent module URLs should return to Review Centre when the character gate is closed."); Assert(locationsAction.Contains("RedirectToReviewCentreWithCharacterGate", StringComparison.Ordinal), "Direct dependent module URLs should return to Review Centre when the character gate is closed.");
} }
static void StoryIntelligenceReviewModuleCompletionIsDurableAndIndependent()
{
var root = PlotLineRoot();
var model = File.ReadAllText(Path.Combine(root, "Models/StoryIntelligencePersistenceModels.cs"));
var repository = File.ReadAllText(Path.Combine(root, "Data/StoryIntelligencePipelineRepository.cs"));
var pipeline = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligencePipelineStateService.cs"));
var onboardingStory = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
var storyService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceService.cs"));
var coreServices = File.ReadAllText(Path.Combine(root, "Services/CoreServices.cs"));
var characterService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceCharacterImportService.cs"));
var locationService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceLocationImportService.cs"));
var assetService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceAssetImportService.cs"));
var relationshipService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceRelationshipImportService.cs"));
var knowledgeService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceKnowledgeImportService.cs"));
var narrativeService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceNarrativeReviewService.cs"));
var migration = File.ReadAllText(Path.Combine(root, "Sql/175_Phase27B_StoryIntelligenceIndependentReviewState.sql"));
var synthesisSql = File.ReadAllText(Path.Combine(root, "Sql/174_Phase27_WholeBookPlotIntelligence.sql"));
foreach (var column in new[]
{
"CharacterReviewCompletedUtc",
"LocationReviewCompletedUtc",
"AssetReviewCompletedUtc",
"RelationshipReviewCompletedUtc",
"KnowledgeReviewCompletedUtc",
"NarrativeReviewCompletedUtc"
})
{
Assert(model.Contains($"public DateTime? {column}", StringComparison.Ordinal), $"{column} should be exposed on persisted pipeline state.");
Assert(migration.Contains($"ADD {column} datetime2 NULL", StringComparison.Ordinal), $"{column} should be added by migration.");
Assert(migration.Contains($"p.{column}", StringComparison.Ordinal), $"{column} should be returned by pipeline read procedures.");
}
Assert(repository.Contains("request.MarkCharacterReviewComplete", StringComparison.Ordinal), "Pipeline upsert should pass durable character completion.");
Assert(repository.Contains("request.MarkNarrativeReviewComplete", StringComparison.Ordinal), "Pipeline upsert should pass durable narrative completion.");
Assert(pipeline.Contains("MarkCharacterReviewComplete = true", StringComparison.Ordinal), "Character import should mark only character review complete.");
Assert(pipeline.Contains("MarkNarrativeReviewComplete = true", StringComparison.Ordinal), "Narrative import should mark only narrative review complete.");
Assert(pipeline.Contains("MarkLocationReviewComplete = true", StringComparison.Ordinal), "Location import should mark durable location completion.");
Assert(pipeline.Contains("MarkAssetReviewComplete = true", StringComparison.Ordinal), "Asset import should mark durable asset completion.");
Assert(pipeline.Contains("MarkRelationshipReviewComplete = true", StringComparison.Ordinal), "Relationship import should mark durable relationship completion.");
Assert(pipeline.Contains("MarkKnowledgeReviewComplete = true", StringComparison.Ordinal), "Knowledge import should mark durable knowledge completion.");
Assert(migration.Contains("CharacterReviewCompletedUtc = CASE WHEN @MarkCharacterReviewComplete = 1 THEN COALESCE(CharacterReviewCompletedUtc", StringComparison.Ordinal), "Pipeline upsert must preserve character completion when other modules update.");
Assert(migration.Contains("NarrativeReviewCompletedUtc = CASE WHEN @MarkNarrativeReviewComplete = 1 THEN COALESCE(NarrativeReviewCompletedUtc", StringComparison.Ordinal), "Pipeline upsert must preserve narrative completion when other modules update.");
Assert(migration.Contains("WHERE NarrativeReviewCompletedUtc IS NULL", StringComparison.Ordinal), "Migration repair should be non-destructive.");
Assert(onboardingStory.Contains("pipeline?.CharacterReviewCompletedUtc is not null", StringComparison.Ordinal), "Review Centre should hydrate character completion from durable state.");
Assert(onboardingStory.Contains("pipeline?.LocationReviewCompletedUtc is not null", StringComparison.Ordinal), "Review Centre should hydrate location completion independently.");
Assert(onboardingStory.Contains("pipeline?.AssetReviewCompletedUtc is not null", StringComparison.Ordinal), "Review Centre should hydrate asset completion independently.");
Assert(onboardingStory.Contains("pipeline?.RelationshipReviewCompletedUtc is not null", StringComparison.Ordinal), "Review Centre should hydrate relationship completion independently.");
Assert(onboardingStory.Contains("pipeline?.KnowledgeReviewCompletedUtc is not null", StringComparison.Ordinal), "Review Centre should hydrate knowledge completion independently.");
Assert(onboardingStory.Contains("pipeline?.NarrativeReviewCompletedUtc is not null", StringComparison.Ordinal), "Review Centre should hydrate narrative completion independently.");
Assert(onboardingStory.Contains("batch.CharacterStageComplete = state.CharacterReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should hydrate character completion from durable state only.");
Assert(!onboardingStory.Contains("state.CurrentStage is StoryIntelligencePipelineStages.LocationReview", StringComparison.Ordinal), "Resume should not infer character completion from post-character stage ordinals.");
Assert(characterService.Contains("pipeline?.CharacterReviewCompletedUtc is not null", StringComparison.Ordinal) && characterService.Contains("Candidates = []", StringComparison.Ordinal), "Completed character review should not resurrect stale scene candidates.");
Assert(locationService.Contains("pipeline?.LocationReviewCompletedUtc is not null", StringComparison.Ordinal), "Completed location review should not resurrect stale scene candidates.");
Assert(assetService.Contains("pipeline?.AssetReviewCompletedUtc is not null", StringComparison.Ordinal), "Completed asset review should not resurrect stale scene candidates.");
Assert(relationshipService.Contains("pipeline?.RelationshipReviewCompletedUtc is not null", StringComparison.Ordinal), "Completed relationship review should not resurrect stale scene candidates.");
Assert(knowledgeService.Contains("pipeline?.KnowledgeReviewCompletedUtc is not null", StringComparison.Ordinal), "Completed knowledge review should not resurrect stale scene candidates.");
Assert(narrativeService.Contains("pipeline?.NarrativeReviewCompletedUtc is not null", StringComparison.Ordinal), "Completed plot review should not resurrect stale plot candidates.");
Assert(!storyService.Contains("StoryIntelligenceReadyForLocations", StringComparison.Ordinal), "Dashboard should not use old post-character location readiness ordinals.");
Assert(!storyService.Contains("StoryIntelligenceReadyForAssets", StringComparison.Ordinal), "Dashboard should not use old post-character asset readiness ordinals.");
Assert(!storyService.Contains("StoryIntelligenceReadyForRelationships", StringComparison.Ordinal), "Dashboard should not use old post-character relationship readiness ordinals.");
Assert(!storyService.Contains("StoryIntelligenceReadyForKnowledge", StringComparison.Ordinal), "Dashboard should not use old post-character knowledge readiness ordinals.");
Assert(coreServices.Contains("pipeline.CharacterReviewCompletedUtc is null", StringComparison.Ordinal), "Book action should not show character review required once durable character completion exists.");
Assert(synthesisSql.Contains("AND Status = N'Pending'", StringComparison.Ordinal), "Whole-book candidate regeneration should preserve reviewed plot decisions.");
Assert(synthesisSql.Contains("Status IN (N'Queued', N'Running', N'Completed')", StringComparison.Ordinal), "Synthesis queue should preserve current completed runs rather than replacing them.");
}
static void StoryIntelligenceReviewModulePagesOmitWizardChrome() static void StoryIntelligenceReviewModulePagesOmitWizardChrome()
{ {
var root = PlotLineRoot(); var root = PlotLineRoot();
@ -3566,16 +3638,16 @@ static void StoryIntelligenceReviewCentreCompletionIsCategoryScoped()
var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
var reviewView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml")); var reviewView = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceReviewCentre.cshtml"));
Assert(onboardingStory.Contains("LocationStageComplete = locationReview.IsComplete", StringComparison.Ordinal), "Location tile completion should come from the Location review service."); Assert(onboardingStory.Contains("LocationStageComplete = locationComplete", StringComparison.Ordinal), "Location tile completion should come from scoped durable/review state.");
Assert(onboardingStory.Contains("AssetStageComplete = assetReview.IsComplete", StringComparison.Ordinal), "Asset tile completion should come from the Asset review service."); Assert(onboardingStory.Contains("AssetStageComplete = assetComplete", StringComparison.Ordinal), "Asset tile completion should come from scoped durable/review state.");
Assert(onboardingStory.Contains("RelationshipStageComplete = relationshipReview.IsComplete", StringComparison.Ordinal), "Relationship tile completion should come from the Relationship review service."); Assert(onboardingStory.Contains("RelationshipStageComplete = relationshipComplete", StringComparison.Ordinal), "Relationship tile completion should come from scoped durable/review state.");
Assert(onboardingStory.Contains("KnowledgeStageComplete = knowledgeReview.IsComplete", StringComparison.Ordinal), "Knowledge tile completion should come from the Knowledge review service."); Assert(onboardingStory.Contains("KnowledgeStageComplete = knowledgeComplete", StringComparison.Ordinal), "Knowledge tile completion should come from scoped durable/review state.");
Assert(onboardingStory.Contains("NarrativeStageComplete = narrative.IsComplete", StringComparison.Ordinal), "Narrative tile completion should come from the Narrative review service."); Assert(onboardingStory.Contains("NarrativeStageComplete = narrativeComplete", StringComparison.Ordinal), "Narrative tile completion should come from scoped durable/review state.");
Assert(onboardingStory.Contains("batch.LocationStageComplete = false;", StringComparison.Ordinal), "Resume should not hydrate Location completion from the single pipeline stage marker."); Assert(onboardingStory.Contains("batch.LocationStageComplete = state.LocationReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should hydrate Location completion from durable state.");
Assert(onboardingStory.Contains("batch.AssetStageComplete = false;", StringComparison.Ordinal), "Resume should not hydrate Asset completion from the single pipeline stage marker."); Assert(onboardingStory.Contains("batch.AssetStageComplete = state.AssetReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should hydrate Asset completion from durable state.");
Assert(onboardingStory.Contains("batch.RelationshipStageComplete = false;", StringComparison.Ordinal), "Resume should not hydrate Relationship completion from the single pipeline stage marker."); Assert(onboardingStory.Contains("batch.RelationshipStageComplete = state.RelationshipReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should hydrate Relationship completion from durable state.");
Assert(onboardingStory.Contains("batch.KnowledgeStageComplete = false;", StringComparison.Ordinal), "Resume should not hydrate Knowledge completion from the single pipeline stage marker."); Assert(onboardingStory.Contains("batch.KnowledgeStageComplete = state.KnowledgeReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should hydrate Knowledge completion from durable state.");
Assert(onboardingStory.Contains("batch.NarrativeStageComplete = false;", StringComparison.Ordinal), "Resume should not hydrate Narrative completion from the single pipeline stage marker."); Assert(onboardingStory.Contains("batch.NarrativeStageComplete = state.NarrativeReviewCompletedUtc is not null", StringComparison.Ordinal), "Resume should hydrate Narrative completion from durable state.");
Assert(reviewView.Contains("Model.PipelineDashboard.RelationshipStageComplete", StringComparison.Ordinal), "Review Centre should render the scoped dashboard completion flags."); Assert(reviewView.Contains("Model.PipelineDashboard.RelationshipStageComplete", StringComparison.Ordinal), "Review Centre should render the scoped dashboard completion flags.");
var locationGet = ExtractMethod(controller, "StoryIntelligenceLocations"); var locationGet = ExtractMethod(controller, "StoryIntelligenceLocations");

View File

@ -70,7 +70,13 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
request.CurrentReviewStage, request.CurrentReviewStage,
request.Status, request.Status,
request.CompletedUtc, request.CompletedUtc,
request.LastRunID request.LastRunID,
request.MarkCharacterReviewComplete,
request.MarkLocationReviewComplete,
request.MarkAssetReviewComplete,
request.MarkRelationshipReviewComplete,
request.MarkKnowledgeReviewComplete,
request.MarkNarrativeReviewComplete
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }

View File

@ -62,6 +62,12 @@ public sealed class StoryIntelligenceBookPipelineState
public string? CurrentReviewStage { get; init; } public string? CurrentReviewStage { get; init; }
public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview; public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview;
public DateTime? CompletedUtc { get; init; } public DateTime? CompletedUtc { get; init; }
public DateTime? CharacterReviewCompletedUtc { get; init; }
public DateTime? LocationReviewCompletedUtc { get; init; }
public DateTime? AssetReviewCompletedUtc { get; init; }
public DateTime? RelationshipReviewCompletedUtc { get; init; }
public DateTime? KnowledgeReviewCompletedUtc { get; init; }
public DateTime? NarrativeReviewCompletedUtc { get; init; }
public int? LastRunID { get; init; } public int? LastRunID { get; init; }
public DateTime CreatedUtc { get; init; } public DateTime CreatedUtc { get; init; }
public DateTime UpdatedUtc { get; init; } public DateTime UpdatedUtc { get; init; }
@ -130,6 +136,12 @@ public sealed class StoryIntelligenceBookPipelineSaveRequest
public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview; public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview;
public DateTime? CompletedUtc { get; init; } public DateTime? CompletedUtc { get; init; }
public int? LastRunID { get; init; } public int? LastRunID { get; init; }
public bool MarkCharacterReviewComplete { get; init; }
public bool MarkLocationReviewComplete { get; init; }
public bool MarkAssetReviewComplete { get; init; }
public bool MarkRelationshipReviewComplete { get; init; }
public bool MarkKnowledgeReviewComplete { get; init; }
public bool MarkNarrativeReviewComplete { get; init; }
} }
public sealed class StoryIntelligenceCommittedRunSummary public sealed class StoryIntelligenceCommittedRunSummary

View File

@ -1382,8 +1382,9 @@ public sealed class BookService(
} }
var isInProgress = string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase); var isInProgress = string.Equals(pipeline.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase);
var needsCharacterReview = string.Equals(pipeline.CurrentReviewStage, StoryIntelligencePipelineStages.CharacterReview, StringComparison.OrdinalIgnoreCase) var needsCharacterReview = pipeline.CharacterReviewCompletedUtc is null
|| string.Equals(pipeline.CurrentStage, StoryIntelligencePipelineStages.CharacterReview, StringComparison.OrdinalIgnoreCase); && (string.Equals(pipeline.CurrentReviewStage, StoryIntelligencePipelineStages.CharacterReview, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pipeline.CurrentStage, StoryIntelligencePipelineStages.CharacterReview, StringComparison.OrdinalIgnoreCase));
return new StoryIntelligenceBookActionViewModel return new StoryIntelligenceBookActionViewModel
{ {
HasCanonicalSceneSources = hasCanonicalSceneSources, HasCanonicalSceneSources = hasCanonicalSceneSources,

View File

@ -647,7 +647,13 @@ public sealed class OnboardingStoryIntelligenceService(
var narrative = await narrativeReview.GetReviewAsync(batch); var narrative = await narrativeReview.GetReviewAsync(batch);
var plotSynthesisRun = await wholeBookPlotIntelligence.GetCurrentAsync(batch.BookID); var plotSynthesisRun = await wholeBookPlotIntelligence.GetCurrentAsync(batch.BookID);
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID); var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
var narrativePendingCount = narrative.Candidates.Count; var characterComplete = batch.CharacterStageComplete || pipeline?.CharacterReviewCompletedUtc is not null || characterReview.IsComplete;
var locationComplete = batch.LocationStageComplete || pipeline?.LocationReviewCompletedUtc is not null || locationReview.IsComplete;
var assetComplete = batch.AssetStageComplete || pipeline?.AssetReviewCompletedUtc is not null || assetReview.IsComplete;
var relationshipComplete = batch.RelationshipStageComplete || pipeline?.RelationshipReviewCompletedUtc is not null || relationshipReview.IsComplete;
var knowledgeComplete = batch.KnowledgeStageComplete || pipeline?.KnowledgeReviewCompletedUtc is not null || knowledgeReview.IsComplete;
var narrativeComplete = batch.NarrativeStageComplete || pipeline?.NarrativeReviewCompletedUtc is not null || narrative.IsComplete;
var narrativePendingCount = narrativeComplete ? 0 : narrative.Candidates.Count;
var narrativeThreadCount = narrative.Candidates.Count(candidate => var narrativeThreadCount = narrative.Candidates.Count(candidate =>
string.Equals(candidate.CandidateType, StoryIntelligenceNarrativeCandidateTypes.Thread, StringComparison.OrdinalIgnoreCase)); string.Equals(candidate.CandidateType, StoryIntelligenceNarrativeCandidateTypes.Thread, StringComparison.OrdinalIgnoreCase));
var narrativeEventCount = narrative.Candidates.Sum(candidate => candidate.Events.Count); var narrativeEventCount = narrative.Candidates.Sum(candidate => candidate.Events.Count);
@ -691,24 +697,24 @@ public sealed class OnboardingStoryIntelligenceService(
SceneAnalysisComplete = pipeline is not null && SceneAnalysisComplete(pipeline), SceneAnalysisComplete = pipeline is not null && SceneAnalysisComplete(pipeline),
CharactersIdentified = characterReview.Candidates.Count + batch.CharacterDecisions.Count, CharactersIdentified = characterReview.Candidates.Count + batch.CharacterDecisions.Count,
CharactersCreatedOrLinked = batch.CharacterDecisions.Count(decision => decision.CreatedOrLinked), CharactersCreatedOrLinked = batch.CharacterDecisions.Count(decision => decision.CreatedOrLinked),
CharacterStageComplete = characterReview.IsComplete, CharacterStageComplete = characterComplete,
LocationsIdentified = locationReview.Candidates.Count + batch.LocationDecisions.Count, LocationsIdentified = locationReview.Candidates.Count + batch.LocationDecisions.Count,
LocationsCreatedOrLinked = batch.LocationDecisions.Count(decision => decision.CreatedOrLinked), LocationsCreatedOrLinked = batch.LocationDecisions.Count(decision => decision.CreatedOrLinked),
LocationStageComplete = locationReview.IsComplete, LocationStageComplete = locationComplete,
AssetsIdentified = assetReview.Candidates.Count + batch.AssetDecisions.Count, AssetsIdentified = assetReview.Candidates.Count + batch.AssetDecisions.Count,
AssetsCreatedOrLinked = batch.AssetDecisions.Count(decision => decision.CreatedOrLinked), AssetsCreatedOrLinked = batch.AssetDecisions.Count(decision => decision.CreatedOrLinked),
AssetStageComplete = assetReview.IsComplete, AssetStageComplete = assetComplete,
RelationshipsIdentified = relationshipReview.Candidates.Count + batch.RelationshipDecisions.Count, RelationshipsIdentified = relationshipReview.Candidates.Count + batch.RelationshipDecisions.Count,
RelationshipsCreatedOrLinked = batch.RelationshipDecisions.Count(decision => decision.CreatedOrLinked), RelationshipsCreatedOrLinked = batch.RelationshipDecisions.Count(decision => decision.CreatedOrLinked),
RelationshipStageComplete = relationshipReview.IsComplete, RelationshipStageComplete = relationshipComplete,
KnowledgeIdentified = knowledgeReview.Candidates.Count + batch.KnowledgeDecisions.Count, KnowledgeIdentified = knowledgeReview.Candidates.Count + batch.KnowledgeDecisions.Count,
KnowledgeCreatedOrLinked = batch.KnowledgeDecisions.Count(decision => decision.CreatedOrLinked), KnowledgeCreatedOrLinked = batch.KnowledgeDecisions.Count(decision => decision.CreatedOrLinked),
KnowledgeStageComplete = knowledgeReview.IsComplete, KnowledgeStageComplete = knowledgeComplete,
NarrativeCandidatesIdentified = narrative.Candidates.Count + (batch.LastNarrativeImportResult?.TotalReviewed ?? 0), NarrativeCandidatesIdentified = narrative.Candidates.Count + (batch.LastNarrativeImportResult?.TotalReviewed ?? 0),
NarrativeCandidatesApplied = (batch.LastNarrativeImportResult?.PlotLinesCreated ?? 0) NarrativeCandidatesApplied = (batch.LastNarrativeImportResult?.PlotLinesCreated ?? 0)
+ (batch.LastNarrativeImportResult?.PlotLinesMerged ?? 0) + (batch.LastNarrativeImportResult?.PlotLinesMerged ?? 0)
+ (batch.LastNarrativeImportResult?.ThreadsCreated ?? 0), + (batch.LastNarrativeImportResult?.ThreadsCreated ?? 0),
NarrativeStageComplete = narrative.IsComplete NarrativeStageComplete = narrativeComplete
} }
}; };
} }
@ -1045,24 +1051,12 @@ public sealed class OnboardingStoryIntelligenceService(
Items = batchItems Items = batchItems
}; };
var characterStageComplete = HasCompletedStage(state, StoryIntelligencePipelineStages.CharacterImport) batch.CharacterStageComplete = state.CharacterReviewCompletedUtc is not null;
|| state.CurrentStage is StoryIntelligencePipelineStages.LocationReview batch.LocationStageComplete = state.LocationReviewCompletedUtc is not null;
or StoryIntelligencePipelineStages.LocationImport batch.AssetStageComplete = state.AssetReviewCompletedUtc is not null;
or StoryIntelligencePipelineStages.AssetReview batch.RelationshipStageComplete = state.RelationshipReviewCompletedUtc is not null;
or StoryIntelligencePipelineStages.AssetImport batch.KnowledgeStageComplete = state.KnowledgeReviewCompletedUtc is not null;
or StoryIntelligencePipelineStages.RelationshipReview batch.NarrativeStageComplete = state.NarrativeReviewCompletedUtc is not null;
or StoryIntelligencePipelineStages.RelationshipImport
or StoryIntelligencePipelineStages.KnowledgeReview
or StoryIntelligencePipelineStages.KnowledgeImport
or StoryIntelligencePipelineStages.NarrativeReview
or StoryIntelligencePipelineStages.NarrativeImport
or StoryIntelligencePipelineStages.Complete;
batch.CharacterStageComplete = characterStageComplete;
batch.LocationStageComplete = false;
batch.AssetStageComplete = false;
batch.RelationshipStageComplete = false;
batch.KnowledgeStageComplete = false;
batch.NarrativeStageComplete = false;
await batchStore.SaveAsync(batch); await batchStore.SaveAsync(batch);

View File

@ -56,6 +56,19 @@ public sealed class StoryIntelligenceAssetImportService(
public async Task<StoryIntelligenceAssetReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch) public async Task<StoryIntelligenceAssetReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
{ {
var data = await BuildCandidateDataAsync(batch); var data = await BuildCandidateDataAsync(batch);
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
if (batch.AssetStageComplete || pipeline?.AssetReviewCompletedUtc is not null)
{
return new StoryIntelligenceAssetReviewViewModel
{
HasCommittedScenes = data.HasCommittedScenes,
CanImport = false,
AlreadyLinkedCount = data.AlreadyLinkedCount,
IsComplete = data.HasCommittedScenes,
Candidates = []
};
}
var decidedKeys = batch.AssetDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); var decidedKeys = batch.AssetDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList(); var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
return new StoryIntelligenceAssetReviewViewModel return new StoryIntelligenceAssetReviewViewModel

View File

@ -72,6 +72,19 @@ public sealed class StoryIntelligenceCharacterImportService(
public async Task<StoryIntelligenceCharacterReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch) public async Task<StoryIntelligenceCharacterReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
{ {
var data = await BuildCandidateDataAsync(batch); var data = await BuildCandidateDataAsync(batch);
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
if (batch.CharacterStageComplete || pipeline?.CharacterReviewCompletedUtc is not null)
{
return new StoryIntelligenceCharacterReviewViewModel
{
HasCommittedScenes = data.HasCommittedScenes,
CanImport = false,
AlreadyLinkedCount = data.AlreadyLinkedCount,
IsComplete = data.HasCommittedScenes,
Candidates = []
};
}
var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList(); var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
var existingIndex = await BuildCharacterIndexAsync(batch.ProjectID); var existingIndex = await BuildCharacterIndexAsync(batch.ProjectID);

View File

@ -34,13 +34,18 @@ public sealed class StoryIntelligenceKnowledgeImportService(
var decidedKeys = batch.KnowledgeDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); var decidedKeys = batch.KnowledgeDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var selectedStateIds = batch.KnowledgeSelections.ToDictionary(selection => selection.Key, selection => selection.KnowledgeStateID, StringComparer.OrdinalIgnoreCase); var selectedStateIds = batch.KnowledgeSelections.ToDictionary(selection => selection.Key, selection => selection.KnowledgeStateID, StringComparer.OrdinalIgnoreCase);
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList(); var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
if (batch.KnowledgeStageComplete || pipeline?.KnowledgeReviewCompletedUtc is not null)
{
visibleCandidates = [];
}
return new StoryIntelligenceKnowledgeReviewViewModel return new StoryIntelligenceKnowledgeReviewViewModel
{ {
HasCommittedScenes = data.HasCommittedScenes, HasCommittedScenes = data.HasCommittedScenes,
CanImport = visibleCandidates.Count > 0, CanImport = visibleCandidates.Count > 0,
AlreadyLinkedCount = data.AlreadyLinkedCount, AlreadyLinkedCount = data.AlreadyLinkedCount,
IsComplete = data.HasCommittedScenes && visibleCandidates.Count == 0, IsComplete = data.HasCommittedScenes && (visibleCandidates.Count == 0 || batch.KnowledgeStageComplete || pipeline?.KnowledgeReviewCompletedUtc is not null),
KnowledgeStateOptions = lookupData.KnowledgeStates KnowledgeStateOptions = lookupData.KnowledgeStates
.OrderBy(state => state.SortOrder) .OrderBy(state => state.SortOrder)
.ThenBy(state => state.StateName) .ThenBy(state => state.StateName)

View File

@ -70,6 +70,19 @@ public sealed class StoryIntelligenceLocationImportService(
public async Task<StoryIntelligenceLocationReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch) public async Task<StoryIntelligenceLocationReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
{ {
var data = await BuildCandidateDataAsync(batch); var data = await BuildCandidateDataAsync(batch);
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
if (batch.LocationStageComplete || pipeline?.LocationReviewCompletedUtc is not null)
{
return new StoryIntelligenceLocationReviewViewModel
{
HasCommittedScenes = data.HasCommittedScenes,
CanImport = false,
AlreadyLinkedCount = data.AlreadyLinkedCount,
IsComplete = data.HasCommittedScenes,
Candidates = []
};
}
var decidedKeys = batch.LocationDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); var decidedKeys = batch.LocationDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList(); var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
return new StoryIntelligenceLocationReviewViewModel return new StoryIntelligenceLocationReviewViewModel

View File

@ -36,8 +36,11 @@ public sealed class StoryIntelligenceNarrativeReviewService(
var rows = (await candidates.ListByBookAsync(batch.BookID)) var rows = (await candidates.ListByBookAsync(batch.BookID))
.Where(IsWholeBookCandidate) .Where(IsWholeBookCandidate)
.ToList(); .ToList();
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
var reviewComplete = batch.NarrativeStageComplete || pipeline?.NarrativeReviewCompletedUtc is not null;
var pending = rows var pending = rows
.Where(candidate => string.Equals(candidate.Status, StoryIntelligenceNarrativeCandidateStatuses.Pending, StringComparison.OrdinalIgnoreCase)) .Where(candidate => !reviewComplete
&& string.Equals(candidate.Status, StoryIntelligenceNarrativeCandidateStatuses.Pending, StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
var plotLines = await plots.ListPlotLinesAsync(batch.ProjectID); var plotLines = await plots.ListPlotLinesAsync(batch.ProjectID);
@ -45,7 +48,7 @@ public sealed class StoryIntelligenceNarrativeReviewService(
{ {
HasCommittedScenes = await HasCommittedScenesAsync(batch), HasCommittedScenes = await HasCommittedScenesAsync(batch),
CanImport = pending.Count > 0, CanImport = pending.Count > 0,
IsComplete = rows.Count > 0 && pending.Count == 0, IsComplete = reviewComplete || (rows.Count > 0 && pending.Count == 0),
PlotLineOptions = plotLines PlotLineOptions = plotLines
.OrderBy(plotLine => plotLine.SortOrder) .OrderBy(plotLine => plotLine.SortOrder)
.ThenBy(plotLine => plotLine.PlotLineName) .ThenBy(plotLine => plotLine.PlotLineName)

View File

@ -108,7 +108,8 @@ public sealed class StoryIntelligencePipelineStateService(
CurrentReviewStage = StoryIntelligencePipelineStages.LocationReview, CurrentReviewStage = StoryIntelligencePipelineStages.LocationReview,
Status = StoryIntelligencePipelineStatuses.NeedsReview, Status = StoryIntelligencePipelineStatuses.NeedsReview,
CompletedUtc = null, CompletedUtc = null,
LastRunID = null LastRunID = null,
MarkCharacterReviewComplete = true
}); });
} }
@ -123,7 +124,8 @@ public sealed class StoryIntelligencePipelineStateService(
CurrentReviewStage = StoryIntelligencePipelineStages.AssetReview, CurrentReviewStage = StoryIntelligencePipelineStages.AssetReview,
Status = StoryIntelligencePipelineStatuses.NeedsReview, Status = StoryIntelligencePipelineStatuses.NeedsReview,
CompletedUtc = null, CompletedUtc = null,
LastRunID = null LastRunID = null,
MarkLocationReviewComplete = true
}); });
} }
@ -138,7 +140,8 @@ public sealed class StoryIntelligencePipelineStateService(
CurrentReviewStage = StoryIntelligencePipelineStages.RelationshipReview, CurrentReviewStage = StoryIntelligencePipelineStages.RelationshipReview,
Status = StoryIntelligencePipelineStatuses.NeedsReview, Status = StoryIntelligencePipelineStatuses.NeedsReview,
CompletedUtc = null, CompletedUtc = null,
LastRunID = null LastRunID = null,
MarkAssetReviewComplete = true
}); });
} }
@ -153,7 +156,8 @@ public sealed class StoryIntelligencePipelineStateService(
CurrentReviewStage = StoryIntelligencePipelineStages.KnowledgeReview, CurrentReviewStage = StoryIntelligencePipelineStages.KnowledgeReview,
Status = StoryIntelligencePipelineStatuses.NeedsReview, Status = StoryIntelligencePipelineStatuses.NeedsReview,
CompletedUtc = null, CompletedUtc = null,
LastRunID = null LastRunID = null,
MarkRelationshipReviewComplete = true
}); });
} }
@ -168,7 +172,8 @@ public sealed class StoryIntelligencePipelineStateService(
CurrentReviewStage = StoryIntelligencePipelineStages.NarrativeReview, CurrentReviewStage = StoryIntelligencePipelineStages.NarrativeReview,
Status = StoryIntelligencePipelineStatuses.NeedsReview, Status = StoryIntelligencePipelineStatuses.NeedsReview,
CompletedUtc = null, CompletedUtc = null,
LastRunID = null LastRunID = null,
MarkKnowledgeReviewComplete = true
}); });
} }
@ -183,7 +188,8 @@ public sealed class StoryIntelligencePipelineStateService(
CurrentReviewStage = null, CurrentReviewStage = null,
Status = StoryIntelligencePipelineStatuses.Complete, Status = StoryIntelligencePipelineStatuses.Complete,
CompletedUtc = DateTime.UtcNow, CompletedUtc = DateTime.UtcNow,
LastRunID = null LastRunID = null,
MarkNarrativeReviewComplete = true
}); });
} }

View File

@ -34,12 +34,18 @@ public sealed class StoryIntelligenceRelationshipImportService(
var selectedTypeIds = batch.RelationshipSelections.ToDictionary(selection => selection.Key, selection => selection.RelationshipTypeID, StringComparer.OrdinalIgnoreCase); var selectedTypeIds = batch.RelationshipSelections.ToDictionary(selection => selection.Key, selection => selection.RelationshipTypeID, StringComparer.OrdinalIgnoreCase);
var decidedKeys = batch.RelationshipDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); var decidedKeys = batch.RelationshipDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList(); var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
var pipeline = await pipelineState.GetForBookAsync(batch.BookID, batch.UserID);
if (batch.RelationshipStageComplete || pipeline?.RelationshipReviewCompletedUtc is not null)
{
visibleCandidates = [];
}
return new StoryIntelligenceRelationshipReviewViewModel return new StoryIntelligenceRelationshipReviewViewModel
{ {
HasCommittedScenes = data.HasCommittedScenes, HasCommittedScenes = data.HasCommittedScenes,
CanImport = visibleCandidates.Count > 0, CanImport = visibleCandidates.Count > 0,
AlreadyLinkedCount = data.AlreadyLinkedCount, AlreadyLinkedCount = data.AlreadyLinkedCount,
IsComplete = data.HasCommittedScenes && visibleCandidates.Count == 0, IsComplete = data.HasCommittedScenes && (visibleCandidates.Count == 0 || batch.RelationshipStageComplete || pipeline?.RelationshipReviewCompletedUtc is not null),
RelationshipTypeOptions = typeOptions, RelationshipTypeOptions = typeOptions,
Candidates = visibleCandidates.Select(candidate => new StoryIntelligenceRelationshipReviewCandidateViewModel Candidates = visibleCandidates.Select(candidate => new StoryIntelligenceRelationshipReviewCandidateViewModel
{ {

View File

@ -281,10 +281,7 @@ public sealed class StoryIntelligenceService(
StoryIntelligencePipelineStages.LocationReview => "Review Centre", StoryIntelligencePipelineStages.LocationReview => "Review Centre",
StoryIntelligencePipelineStages.CharacterReview => "Character Resolution Needed", StoryIntelligencePipelineStages.CharacterReview => "Character Resolution Needed",
StoryIntelligencePipelineStages.SceneReview => "Needs Scene Review", StoryIntelligencePipelineStages.SceneReview => "Needs Scene Review",
_ when StoryIntelligenceReadyForKnowledge(state) => "Review Centre", _ when StoryIntelligenceCharacterReviewComplete(state) => "Review Centre",
_ when StoryIntelligenceReadyForRelationships(state) => "Review Centre",
_ when StoryIntelligenceReadyForAssets(state) => "Review Centre",
_ when StoryIntelligenceReadyForLocations(state) => "Review Centre",
_ => string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase) _ => string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase)
? "Story Intelligence In Progress" ? "Story Intelligence In Progress"
: "Story Intelligence Needs Review" : "Story Intelligence Needs Review"
@ -304,36 +301,12 @@ public sealed class StoryIntelligenceService(
StoryIntelligencePipelineStages.LocationReview => "Open the Review Centre and choose which saved category to handle.", StoryIntelligencePipelineStages.LocationReview => "Open the Review Centre and choose which saved category to handle.",
StoryIntelligencePipelineStages.CharacterReview => "Resolve character identities before reviewing dependent story intelligence.", StoryIntelligencePipelineStages.CharacterReview => "Resolve character identities before reviewing dependent story intelligence.",
StoryIntelligencePipelineStages.SceneReview => "Story Intelligence analysis has not finished for this book.", StoryIntelligencePipelineStages.SceneReview => "Story Intelligence analysis has not finished for this book.",
_ when StoryIntelligenceReadyForKnowledge(state) => "Open the Review Centre and choose which saved category to handle.", _ when StoryIntelligenceCharacterReviewComplete(state) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForRelationships(state) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForAssets(state) => "Open the Review Centre and choose which saved category to handle.",
_ when StoryIntelligenceReadyForLocations(state) => "Open the Review Centre and choose which saved category to handle.",
_ => "Open the Review Centre and choose which saved category to handle." _ => "Open the Review Centre and choose which saved category to handle."
}; };
private static bool StoryIntelligenceReadyForKnowledge(StoryIntelligenceBookPipelineState state) private static bool StoryIntelligenceCharacterReviewComplete(StoryIntelligenceBookPipelineState state)
=> string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.RelationshipImport, StringComparison.OrdinalIgnoreCase) => state.CharacterReviewCompletedUtc is not null;
|| state.CurrentStage is StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport;
private static bool StoryIntelligenceReadyForRelationships(StoryIntelligenceBookPipelineState state)
=> string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.AssetImport, StringComparison.OrdinalIgnoreCase)
|| state.CurrentStage is StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
or StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport;
private static bool StoryIntelligenceReadyForAssets(StoryIntelligenceBookPipelineState state)
=> string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.LocationImport, StringComparison.OrdinalIgnoreCase)
|| state.CurrentStage is StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
or StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport
|| (string.Equals(state.CurrentStage, StoryIntelligencePipelineStages.Complete, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.KnowledgeImport, StringComparison.OrdinalIgnoreCase));
private static bool StoryIntelligenceReadyForLocations(StoryIntelligenceBookPipelineState state)
=> string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.CharacterImport, StringComparison.OrdinalIgnoreCase)
|| state.CurrentStage is StoryIntelligencePipelineStages.LocationReview or StoryIntelligencePipelineStages.LocationImport
or StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
or StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport;
private static string FormatElapsed(TimeSpan elapsed) private static string FormatElapsed(TimeSpan elapsed)
=> elapsed.TotalMinutes < 1 => elapsed.TotalMinutes < 1

View File

@ -0,0 +1,255 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'CharacterReviewCompletedUtc') IS NULL
BEGIN
ALTER TABLE dbo.StoryIntelligenceBookPipelines ADD CharacterReviewCompletedUtc datetime2 NULL;
END;
GO
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'LocationReviewCompletedUtc') IS NULL
BEGIN
ALTER TABLE dbo.StoryIntelligenceBookPipelines ADD LocationReviewCompletedUtc datetime2 NULL;
END;
GO
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'AssetReviewCompletedUtc') IS NULL
BEGIN
ALTER TABLE dbo.StoryIntelligenceBookPipelines ADD AssetReviewCompletedUtc datetime2 NULL;
END;
GO
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'RelationshipReviewCompletedUtc') IS NULL
BEGIN
ALTER TABLE dbo.StoryIntelligenceBookPipelines ADD RelationshipReviewCompletedUtc datetime2 NULL;
END;
GO
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'KnowledgeReviewCompletedUtc') IS NULL
BEGIN
ALTER TABLE dbo.StoryIntelligenceBookPipelines ADD KnowledgeReviewCompletedUtc datetime2 NULL;
END;
GO
IF COL_LENGTH(N'dbo.StoryIntelligenceBookPipelines', N'NarrativeReviewCompletedUtc') IS NULL
BEGIN
ALTER TABLE dbo.StoryIntelligenceBookPipelines ADD NarrativeReviewCompletedUtc datetime2 NULL;
END;
GO
UPDATE dbo.StoryIntelligenceBookPipelines
SET CharacterReviewCompletedUtc = COALESCE(CharacterReviewCompletedUtc, UpdatedUtc, SYSUTCDATETIME())
WHERE CharacterReviewCompletedUtc IS NULL
AND
(
LastCompletedStage IN
(
N'CharacterImport',
N'LocationImport',
N'AssetImport',
N'RelationshipImport',
N'KnowledgeImport',
N'NarrativeImport'
)
OR CurrentStage IN
(
N'LocationReview',
N'LocationImport',
N'AssetReview',
N'AssetImport',
N'RelationshipReview',
N'RelationshipImport',
N'KnowledgeReview',
N'KnowledgeImport',
N'NarrativeReview',
N'NarrativeImport',
N'Complete'
)
);
GO
UPDATE dbo.StoryIntelligenceBookPipelines
SET NarrativeReviewCompletedUtc = COALESCE(NarrativeReviewCompletedUtc, CompletedUtc, UpdatedUtc, SYSUTCDATETIME())
WHERE NarrativeReviewCompletedUtc IS NULL
AND LastCompletedStage = N'NarrativeImport'
AND Status = N'Complete';
GO
UPDATE dbo.StoryIntelligenceBookPipelines
SET LocationReviewCompletedUtc = COALESCE(LocationReviewCompletedUtc, UpdatedUtc, SYSUTCDATETIME())
WHERE LocationReviewCompletedUtc IS NULL
AND LastCompletedStage = N'LocationImport';
GO
UPDATE dbo.StoryIntelligenceBookPipelines
SET AssetReviewCompletedUtc = COALESCE(AssetReviewCompletedUtc, UpdatedUtc, SYSUTCDATETIME())
WHERE AssetReviewCompletedUtc IS NULL
AND LastCompletedStage = N'AssetImport';
GO
UPDATE dbo.StoryIntelligenceBookPipelines
SET RelationshipReviewCompletedUtc = COALESCE(RelationshipReviewCompletedUtc, UpdatedUtc, SYSUTCDATETIME())
WHERE RelationshipReviewCompletedUtc IS NULL
AND LastCompletedStage = N'RelationshipImport';
GO
UPDATE dbo.StoryIntelligenceBookPipelines
SET KnowledgeReviewCompletedUtc = COALESCE(KnowledgeReviewCompletedUtc, UpdatedUtc, SYSUTCDATETIME())
WHERE KnowledgeReviewCompletedUtc IS NULL
AND LastCompletedStage = N'KnowledgeImport';
GO
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_GetByBook
@BookID int
AS
BEGIN
SET NOCOUNT ON;
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, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
FROM dbo.StoryIntelligenceBookPipelines p
INNER JOIN dbo.Projects project ON project.ProjectID = p.ProjectID
INNER JOIN dbo.Books book ON book.BookID = p.BookID
WHERE p.BookID = @BookID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_Upsert
@ProjectID int,
@BookID int,
@CurrentStage nvarchar(80),
@LastCompletedStage nvarchar(80) = NULL,
@CurrentReviewStage nvarchar(80) = NULL,
@Status nvarchar(40),
@CompletedUtc datetime2 = NULL,
@LastRunID int = NULL,
@MarkCharacterReviewComplete bit = 0,
@MarkLocationReviewComplete bit = 0,
@MarkAssetReviewComplete bit = 0,
@MarkRelationshipReviewComplete bit = 0,
@MarkKnowledgeReviewComplete bit = 0,
@MarkNarrativeReviewComplete bit = 0
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM dbo.StoryIntelligenceBookPipelines WHERE BookID = @BookID)
BEGIN
UPDATE dbo.StoryIntelligenceBookPipelines
SET ProjectID = @ProjectID,
CurrentStage = @CurrentStage,
LastCompletedStage = @LastCompletedStage,
CurrentReviewStage = @CurrentReviewStage,
Status = @Status,
CompletedUtc = @CompletedUtc,
LastRunID = COALESCE(@LastRunID, LastRunID),
CharacterReviewCompletedUtc = CASE WHEN @MarkCharacterReviewComplete = 1 THEN COALESCE(CharacterReviewCompletedUtc, SYSUTCDATETIME()) ELSE CharacterReviewCompletedUtc END,
LocationReviewCompletedUtc = CASE WHEN @MarkLocationReviewComplete = 1 THEN COALESCE(LocationReviewCompletedUtc, SYSUTCDATETIME()) ELSE LocationReviewCompletedUtc END,
AssetReviewCompletedUtc = CASE WHEN @MarkAssetReviewComplete = 1 THEN COALESCE(AssetReviewCompletedUtc, SYSUTCDATETIME()) ELSE AssetReviewCompletedUtc END,
RelationshipReviewCompletedUtc = CASE WHEN @MarkRelationshipReviewComplete = 1 THEN COALESCE(RelationshipReviewCompletedUtc, SYSUTCDATETIME()) ELSE RelationshipReviewCompletedUtc END,
KnowledgeReviewCompletedUtc = CASE WHEN @MarkKnowledgeReviewComplete = 1 THEN COALESCE(KnowledgeReviewCompletedUtc, SYSUTCDATETIME()) ELSE KnowledgeReviewCompletedUtc END,
NarrativeReviewCompletedUtc = CASE WHEN @MarkNarrativeReviewComplete = 1 THEN COALESCE(NarrativeReviewCompletedUtc, SYSUTCDATETIME()) ELSE NarrativeReviewCompletedUtc END,
UpdatedUtc = SYSUTCDATETIME()
WHERE BookID = @BookID;
END
ELSE
BEGIN
INSERT dbo.StoryIntelligenceBookPipelines
(ProjectID, BookID, CurrentStage, LastCompletedStage, CurrentReviewStage, Status, CompletedUtc, LastRunID,
CharacterReviewCompletedUtc, LocationReviewCompletedUtc, AssetReviewCompletedUtc,
RelationshipReviewCompletedUtc, KnowledgeReviewCompletedUtc, NarrativeReviewCompletedUtc)
VALUES
(@ProjectID, @BookID, @CurrentStage, @LastCompletedStage, @CurrentReviewStage, @Status, @CompletedUtc, @LastRunID,
CASE WHEN @MarkCharacterReviewComplete = 1 THEN SYSUTCDATETIME() ELSE NULL END,
CASE WHEN @MarkLocationReviewComplete = 1 THEN SYSUTCDATETIME() ELSE NULL END,
CASE WHEN @MarkAssetReviewComplete = 1 THEN SYSUTCDATETIME() ELSE NULL END,
CASE WHEN @MarkRelationshipReviewComplete = 1 THEN SYSUTCDATETIME() ELSE NULL END,
CASE WHEN @MarkKnowledgeReviewComplete = 1 THEN SYSUTCDATETIME() ELSE NULL END,
CASE WHEN @MarkNarrativeReviewComplete = 1 THEN SYSUTCDATETIME() ELSE NULL END);
END;
EXEC dbo.StoryIntelligenceBookPipeline_GetByBook @BookID = @BookID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_GetByBookForUser
@BookID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
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, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
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
WHERE p.BookID = @BookID
AND book.IsArchived = 0
AND project.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_GetByIDForUser
@StoryIntelligenceBookPipelineID int,
@UserID int
AS
BEGIN
SET NOCOUNT ON;
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, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
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
WHERE p.StoryIntelligenceBookPipelineID = @StoryIntelligenceBookPipelineID
AND book.IsArchived = 0
AND project.IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_ListForUser
@UserID int
AS
BEGIN
SET NOCOUNT ON;
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, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
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
WHERE book.IsArchived = 0
AND project.IsArchived = 0
ORDER BY
CASE p.Status WHEN N'NeedsReview' THEN 0 WHEN N'InProgress' THEN 1 ELSE 2 END,
p.UpdatedUtc DESC;
END;
GO