diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 38fc140..2aac677 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -2643,11 +2643,15 @@ static void StoryIntelligenceNarrativeStageFollowsKnowledge() var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs")); var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs")); var knowledgeComplete = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceKnowledgeComplete.cshtml")); + var pipelineConstraint = File.ReadAllText(Path.Combine(root, "Sql/156_Phase25A_NarrativeReviewPipelineConstraint.sql")); + var knowledgeImport = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceKnowledgeImportService.cs")); Assert(stages.Contains("NarrativeReview", StringComparison.Ordinal) && stages.Contains("NarrativeImport", StringComparison.Ordinal), "Pipeline stages should include narrative review/import."); Assert(stages.Contains("LastCompletedStage, StoryIntelligencePipelineStages.NarrativeImport", StringComparison.Ordinal), "Pipeline completion should require narrative import."); Assert(pipeline.Contains("CurrentStage = StoryIntelligencePipelineStages.NarrativeReview", StringComparison.Ordinal), "Knowledge import should lead to narrative review."); Assert(pipeline.Contains("RecordNarrativeImportAsync", StringComparison.Ordinal), "Narrative import should mark the pipeline complete."); + Assert(pipelineConstraint.Contains("N'NarrativeReview'", StringComparison.Ordinal) && pipelineConstraint.Contains("N'NarrativeImport'", StringComparison.Ordinal), "Database pipeline constraint should allow narrative stages."); + Assert(knowledgeImport.Contains("hasUndecidedCandidates", StringComparison.Ordinal), "Knowledge review should be able to advance when prior decisions already imported every candidate."); Assert(onboarding.Contains("StoryIntelligenceResumeRoutes.Narrative", StringComparison.Ordinal), "Resume logic should route to narrative review."); Assert(controller.Contains("StoryIntelligenceNarrative", StringComparison.Ordinal), "Onboarding controller should expose the narrative review stage."); Assert(knowledgeComplete.Contains("Review Plot Lines & Threads", StringComparison.Ordinal), "Knowledge completion should lead to narrative review."); diff --git a/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs b/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs index 4c03bd2..bbb2daf 100644 --- a/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs +++ b/PlotLine/Services/StoryIntelligenceKnowledgeImportService.cs @@ -79,15 +79,23 @@ public sealed class StoryIntelligenceKnowledgeImportService( SaveKnowledgeSelections(batch, choices.Values); if (choices.Count == 0) { - if (data.Candidates.Count == 0) + var decidedKeys = batch.KnowledgeDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); + var hasUndecidedCandidates = data.Candidates.Any(candidate => !decidedKeys.Contains(candidate.Key)); + if (!hasUndecidedCandidates) { batch.KnowledgeStageComplete = true; - batch.LastKnowledgeImportResult = new StoryIntelligenceKnowledgeImportBatchResult(); + batch.LastKnowledgeImportResult ??= new StoryIntelligenceKnowledgeImportBatchResult + { + KnowledgeCreated = batch.KnowledgeDecisions.Count(decision => string.Equals(decision.Action, StoryIntelligenceKnowledgeImportActions.CreateNew, StringComparison.OrdinalIgnoreCase)), + KnowledgeLinked = batch.KnowledgeDecisions.Count(decision => string.Equals(decision.Action, StoryIntelligenceKnowledgeImportActions.LinkExisting, 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)) + }; await pipelineState.RecordKnowledgeImportAsync(batch.ProjectID, batch.BookID); return new StoryIntelligenceImportCommitResult { Success = true, - Message = "Knowledge review completed. No knowledge decisions were waiting." + Message = "Knowledge review completed. No knowledge decisions are waiting." }; } diff --git a/PlotLine/Sql/156_Phase25A_NarrativeReviewPipelineConstraint.sql b/PlotLine/Sql/156_Phase25A_NarrativeReviewPipelineConstraint.sql new file mode 100644 index 0000000..fbd7714 --- /dev/null +++ b/PlotLine/Sql/156_Phase25A_NarrativeReviewPipelineConstraint.sql @@ -0,0 +1,42 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +IF OBJECT_ID(N'dbo.StoryIntelligenceBookPipelines', N'U') IS NOT NULL +BEGIN + IF EXISTS + ( + SELECT 1 + FROM sys.check_constraints + WHERE name = N'CK_StoryIntelligenceBookPipelines_CurrentStage' + AND parent_object_id = OBJECT_ID(N'dbo.StoryIntelligenceBookPipelines') + ) + BEGIN + ALTER TABLE dbo.StoryIntelligenceBookPipelines + DROP CONSTRAINT CK_StoryIntelligenceBookPipelines_CurrentStage; + END; + + ALTER TABLE dbo.StoryIntelligenceBookPipelines + ADD CONSTRAINT CK_StoryIntelligenceBookPipelines_CurrentStage + CHECK (CurrentStage IN + ( + N'Chapters', + N'SceneReview', + N'SceneImport', + N'CharacterReview', + N'CharacterImport', + N'LocationReview', + N'LocationImport', + N'AssetReview', + N'AssetImport', + N'RelationshipReview', + N'RelationshipImport', + N'KnowledgeReview', + N'KnowledgeImport', + N'NarrativeReview', + N'NarrativeImport', + N'Complete' + )); +END; +GO