SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO IF OBJECT_ID(N'dbo.GenreMetricPresets', N'U') IS NULL BEGIN CREATE TABLE dbo.GenreMetricPresets ( GenreMetricPresetID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_GenreMetricPresets PRIMARY KEY, PresetKey nvarchar(50) NOT NULL, PresetName nvarchar(100) NOT NULL, Description nvarchar(500) NULL, SortOrder int NOT NULL, IsActive bit NOT NULL CONSTRAINT DF_GenreMetricPresets_IsActive DEFAULT 1, CONSTRAINT UQ_GenreMetricPresets_PresetKey UNIQUE(PresetKey) ); END; GO IF OBJECT_ID(N'dbo.GenreMetricPresetMetrics', N'U') IS NULL BEGIN CREATE TABLE dbo.GenreMetricPresetMetrics ( GenreMetricPresetID int NOT NULL, MetricTypeID int NOT NULL, SortOrder int NOT NULL, CONSTRAINT PK_GenreMetricPresetMetrics PRIMARY KEY(GenreMetricPresetID, MetricTypeID), CONSTRAINT FK_GenreMetricPresetMetrics_Presets FOREIGN KEY(GenreMetricPresetID) REFERENCES dbo.GenreMetricPresets(GenreMetricPresetID), CONSTRAINT FK_GenreMetricPresetMetrics_MetricTypes FOREIGN KEY(MetricTypeID) REFERENCES dbo.SceneMetricTypes(MetricTypeID) ); END; GO MERGE dbo.GenreMetricPresets AS target USING (VALUES (N'Custom', N'Custom / No preset', N'Use the core PlotLine metrics only.', 10), (N'CrimeThriller', N'Crime / Thriller', N'Suspense, danger, investigation and clues.', 20), (N'Romance', N'Romance', N'Emotional intimacy, conflict, trust, longing and chemistry.', 30), (N'Fantasy', N'Fantasy', N'Wonder, world building, magic, politics and quest progress.', 40), (N'Horror', N'Horror', N'Dread, fear, isolation, gore and helplessness.', 50), (N'HistoricalFiction', N'Historical Fiction', N'Immersion, authenticity and social or political pressure.', 60), (N'LiteraryFiction', N'Literary Fiction', N'Character growth, internal conflict, subtext and theme.', 70) ) AS source (PresetKey, PresetName, Description, SortOrder) ON target.PresetKey = source.PresetKey WHEN MATCHED THEN UPDATE SET PresetName = source.PresetName, Description = source.Description, SortOrder = source.SortOrder, IsActive = 1 WHEN NOT MATCHED THEN INSERT (PresetKey, PresetName, Description, SortOrder, IsActive) VALUES (source.PresetKey, source.PresetName, source.Description, source.SortOrder, 1); GO MERGE dbo.SceneMetricTypes AS target USING (VALUES (N'Suspense', N'Rising uncertainty and anticipation of danger.', 5, 210), (N'Danger', N'Immediate physical, emotional, or strategic risk.', 5, 220), (N'Investigation Progress', N'How far the truth-finding or case-solving thread advances.', 5, 230), (N'Clue Density', N'How much meaningful clue material appears in the scene.', 4, 240), (N'Threat Proximity', N'How close the active threat feels to the characters.', 5, 250), (N'Emotional Intimacy', N'Emotional closeness, vulnerability, and connection.', 5, 310), (N'Relationship Conflict', N'Tension, friction, or opposing needs within the relationship.', 5, 320), (N'Trust', N'How much trust is present, earned, tested, or repaired.', 5, 330), (N'Longing', N'Yearning, desire, separation, or unresolved emotional pull.', 5, 340), (N'Chemistry', N'Spark, attraction, and interpersonal charge.', 5, 350), (N'Wonder', N'Sense of awe, strangeness, discovery, or enchantment.', 5, 410), (N'World Building', N'How much setting, culture, history, or rules are developed.', 4, 420), (N'Magic Presence', N'How strongly magic or the fantastic shapes the scene.', 4, 430), (N'Political Intrigue', N'Power manoeuvring, faction pressure, and strategic schemes.', 5, 440), (N'Quest Progress', N'How much the central journey or mission advances.', 5, 450), (N'Dread', N'Slow-building unease and expectation of something terrible.', 5, 510), (N'Fear', N'Immediate fear or panic experienced by characters or reader.', 5, 520), (N'Isolation', N'How alone, cut off, or unsupported characters feel.', 5, 530), (N'Gore', N'Graphic bodily harm or visceral horror content.', 1, 540), (N'Helplessness', N'Loss of control, trapped choices, or inability to act.', 5, 550), (N'Historical Immersion', N'How strongly the scene evokes its period and setting.', 5, 610), (N'Social Pressure', N'Expectations, class, family, reputation, and public constraint.', 5, 620), (N'Political Tension', N'Period power pressure, public conflict, or institutional risk.', 5, 630), (N'Authenticity', N'How heavily the scene relies on period-specific detail or accuracy.', 5, 640), (N'Cultural Conflict', N'Conflict arising from customs, values, identity, or social change.', 5, 650), (N'Character Growth', N'Visible internal change, self-knowledge, or development.', 5, 710), (N'Internal Conflict', N'Inner contradiction, dilemma, doubt, or self-opposition.', 5, 720), (N'Subtext', N'Unspoken meaning, implication, and tension beneath dialogue or action.', 5, 730), (N'Theme Strength', N'How strongly the scene expresses or complicates the work''s themes.', 5, 740), (N'Emotional Resonance', N'Aftertaste, depth, and lingering emotional impact.', 5, 750) ) AS source (MetricName, Description, DefaultValue, SortOrder) ON target.ProjectID IS NULL AND target.MetricName = source.MetricName WHEN MATCHED THEN UPDATE SET Description = source.Description, MinValue = 1, MaxValue = 10, DefaultValue = source.DefaultValue, SortOrder = source.SortOrder, IsActive = 1 WHEN NOT MATCHED THEN INSERT (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive) VALUES (NULL, source.MetricName, source.Description, 1, 10, source.DefaultValue, source.SortOrder, 1); GO DELETE gpmm FROM dbo.GenreMetricPresetMetrics gpmm INNER JOIN dbo.GenreMetricPresets gmp ON gmp.GenreMetricPresetID = gpmm.GenreMetricPresetID; GO INSERT dbo.GenreMetricPresetMetrics (GenreMetricPresetID, MetricTypeID, SortOrder) SELECT gmp.GenreMetricPresetID, smt.MetricTypeID, rows.SortOrder FROM ( VALUES (N'CrimeThriller', N'Suspense', 210), (N'CrimeThriller', N'Danger', 220), (N'CrimeThriller', N'Investigation Progress', 230), (N'CrimeThriller', N'Clue Density', 240), (N'CrimeThriller', N'Threat Proximity', 250), (N'Romance', N'Emotional Intimacy', 310), (N'Romance', N'Relationship Conflict', 320), (N'Romance', N'Trust', 330), (N'Romance', N'Longing', 340), (N'Romance', N'Chemistry', 350), (N'Fantasy', N'Wonder', 410), (N'Fantasy', N'World Building', 420), (N'Fantasy', N'Magic Presence', 430), (N'Fantasy', N'Political Intrigue', 440), (N'Fantasy', N'Quest Progress', 450), (N'Horror', N'Dread', 510), (N'Horror', N'Fear', 520), (N'Horror', N'Isolation', 530), (N'Horror', N'Gore', 540), (N'Horror', N'Helplessness', 550), (N'HistoricalFiction', N'Historical Immersion', 610), (N'HistoricalFiction', N'Social Pressure', 620), (N'HistoricalFiction', N'Political Tension', 630), (N'HistoricalFiction', N'Authenticity', 640), (N'HistoricalFiction', N'Cultural Conflict', 650), (N'LiteraryFiction', N'Character Growth', 710), (N'LiteraryFiction', N'Internal Conflict', 720), (N'LiteraryFiction', N'Subtext', 730), (N'LiteraryFiction', N'Theme Strength', 740), (N'LiteraryFiction', N'Emotional Resonance', 750) ) rows (PresetKey, MetricName, SortOrder) INNER JOIN dbo.GenreMetricPresets gmp ON gmp.PresetKey = rows.PresetKey INNER JOIN dbo.SceneMetricTypes smt ON smt.ProjectID IS NULL AND smt.MetricName = rows.MetricName WHERE NOT EXISTS ( SELECT 1 FROM dbo.GenreMetricPresetMetrics existing WHERE existing.GenreMetricPresetID = gmp.GenreMetricPresetID AND existing.MetricTypeID = smt.MetricTypeID ); GO CREATE OR ALTER PROCEDURE dbo.GenreMetricPreset_List AS BEGIN SET NOCOUNT ON; SELECT GenreMetricPresetID, PresetKey, PresetName, Description, SortOrder, IsActive FROM dbo.GenreMetricPresets WHERE IsActive = 1 ORDER BY SortOrder, PresetName; END; GO CREATE OR ALTER PROCEDURE dbo.ProjectMetricPreset_Apply @ProjectID int, @GenreMetricPresetKey nvarchar(50) = N'Custom' AS BEGIN SET NOCOUNT ON; IF NOT EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0) RETURN; IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID) BEGIN INSERT dbo.ProjectTimelineSettings (ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings) VALUES (@ProjectID, 1, 1, 1, 1, 1, 1); END; DECLARE @PresetID int = ( SELECT GenreMetricPresetID FROM dbo.GenreMetricPresets WHERE PresetKey = COALESCE(NULLIF(LTRIM(RTRIM(@GenreMetricPresetKey)), N''), N'Custom') AND IsActive = 1 ); DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID; INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder) SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder FROM dbo.SceneMetricTypes mt WHERE mt.ProjectID IS NULL AND mt.IsActive = 1 AND mt.MetricName IN ( N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' ) UNION SELECT @ProjectID, mt.MetricTypeID, gpmm.SortOrder FROM dbo.GenreMetricPresetMetrics gpmm INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = gpmm.MetricTypeID WHERE gpmm.GenreMetricPresetID = @PresetID AND mt.IsActive = 1 AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID); END; GO CREATE OR ALTER PROCEDURE dbo.Project_Save @ProjectID int = NULL, @ProjectName nvarchar(200), @Description nvarchar(max) = NULL, @GenreMetricPresetKey nvarchar(50) = NULL AS BEGIN SET NOCOUNT ON; IF @ProjectID IS NULL OR @ProjectID = 0 BEGIN INSERT dbo.Projects (ProjectName, Description) VALUES (@ProjectName, @Description); SET @ProjectID = CAST(SCOPE_IDENTITY() AS int); EXEC dbo.ProjectMetricPreset_Apply @ProjectID = @ProjectID, @GenreMetricPresetKey = @GenreMetricPresetKey; SELECT @ProjectID AS ProjectID; RETURN; END; UPDATE dbo.Projects SET ProjectName = @ProjectName, Description = @Description, UpdatedDate = SYSUTCDATETIME() WHERE ProjectID = @ProjectID; SELECT @ProjectID AS ProjectID; END; GO CREATE OR ALTER PROCEDURE dbo.SceneMetricType_List @ProjectID int = NULL AS BEGIN SET NOCOUNT ON; DECLARE @HasMetricSettings bit = CASE WHEN @ProjectID IS NOT NULL AND EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END; SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive FROM dbo.SceneMetricTypes mt WHERE mt.IsActive = 1 AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID) AND ( @ProjectID IS NULL OR (@HasMetricSettings = 1 AND EXISTS ( SELECT 1 FROM dbo.ProjectTimelineMetricSettings ptms WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID )) OR (@HasMetricSettings = 0 AND mt.MetricName IN ( N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' )) ) ORDER BY mt.SortOrder, mt.MetricName; END; GO CREATE OR ALTER PROCEDURE dbo.SceneMetric_EnsureDefaults @SceneID int AS BEGIN SET NOCOUNT ON; DECLARE @ProjectID int; SELECT @ProjectID = b.ProjectID FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE s.SceneID = @SceneID; DECLARE @HasMetricSettings bit = CASE WHEN EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END; INSERT dbo.SceneMetricValues (SceneID, MetricTypeID, Value, Notes) SELECT @SceneID, mt.MetricTypeID, mt.DefaultValue, NULL FROM dbo.SceneMetricTypes mt WHERE mt.IsActive = 1 AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID) AND ( (@HasMetricSettings = 1 AND EXISTS ( SELECT 1 FROM dbo.ProjectTimelineMetricSettings ptms WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID )) OR (@HasMetricSettings = 0 AND mt.MetricName IN ( N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' )) ) AND NOT EXISTS ( SELECT 1 FROM dbo.SceneMetricValues smv WHERE smv.SceneID = @SceneID AND smv.MetricTypeID = mt.MetricTypeID ); END; GO CREATE OR ALTER PROCEDURE dbo.SceneMetric_ListByScene @SceneID int AS BEGIN SET NOCOUNT ON; EXEC dbo.SceneMetric_EnsureDefaults @SceneID = @SceneID; DECLARE @ProjectID int; SELECT @ProjectID = b.ProjectID FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE s.SceneID = @SceneID; DECLARE @HasMetricSettings bit = CASE WHEN EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END; SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, mt.DefaultValue, mt.SortOrder, mt.IsActive, smv.SceneID, smv.Value, smv.Notes FROM dbo.SceneMetricTypes mt INNER JOIN dbo.SceneMetricValues smv ON smv.MetricTypeID = mt.MetricTypeID AND smv.SceneID = @SceneID WHERE mt.IsActive = 1 AND ( (@HasMetricSettings = 1 AND EXISTS ( SELECT 1 FROM dbo.ProjectTimelineMetricSettings ptms WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID )) OR (@HasMetricSettings = 0 AND mt.MetricName IN ( N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness', N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery' )) ) ORDER BY mt.SortOrder, mt.MetricName; END; GO CREATE OR ALTER PROCEDURE dbo.Analytics_StoryHealth_Remainder @ProjectID int, @BookID int = NULL, @StartChapterNumber decimal(10,2) = NULL, @EndChapterNumber decimal(10,2) = NULL, @StartSceneNumber decimal(10,2) = NULL, @EndSceneNumber decimal(10,2) = NULL AS BEGIN SET NOCOUNT ON; ;WITH EventScope AS ( SELECT pt.PlotThreadID, MAX(nso.GlobalSceneIndex) AS LastTouchedIndex, MAX(nso.SceneID) AS LastTouchedSceneID, MAX(CONCAT(N'Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle)) AS LastTouchedSceneLabel FROM dbo.PlotThreads pt INNER JOIN dbo.ThreadEvents te ON te.PlotThreadID = pt.PlotThreadID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = te.SceneID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) GROUP BY pt.PlotThreadID ) SELECT pt.PlotThreadID, pt.ThreadTitle, pl.PlotLineID, pl.PlotLineName, pt.Importance, ts.StatusName AS ThreadStatusName, ts.IsOpenStatus, ts.IsResolvedStatus, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID, EventScope.LastTouchedSceneID, EventScope.LastTouchedSceneLabel, CASE WHEN ts.IsOpenStatus = 1 AND pt.Importance >= 8 THEN 1 ELSE 0 END AS IsHighImportanceOpen, CASE WHEN ts.IsOpenStatus = 1 AND EventScope.LastTouchedIndex IS NOT NULL AND (SELECT MAX(GlobalSceneIndex) FROM dbo.NarrativeSceneOrder WHERE ProjectID = @ProjectID AND (@BookID IS NULL OR BookID = @BookID)) - EventScope.LastTouchedIndex > 8 THEN 1 ELSE 0 END AS IsDormant, CASE WHEN EXISTS ( SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Clue Planted' ) AND NOT EXISTS ( SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName IN (N'Payoff', N'Question Answered', N'Revealed', N'Resolved') ) THEN 1 ELSE 0 END AS HasClueWithoutPayoff, CASE WHEN EXISTS ( SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Question Raised' ) AND NOT EXISTS ( SELECT 1 FROM dbo.ThreadEvents te INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID WHERE te.PlotThreadID = pt.PlotThreadID AND tet.TypeName = N'Question Answered' ) THEN 1 ELSE 0 END AS HasQuestionWithoutAnswer FROM dbo.PlotThreads pt INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID LEFT JOIN EventScope ON EventScope.PlotThreadID = pt.PlotThreadID WHERE pl.ProjectID = @ProjectID AND pt.IsArchived = 0 AND pl.IsArchived = 0 AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID) ORDER BY ts.IsOpenStatus DESC, pt.Importance DESC, pl.PlotLineName, pt.ThreadTitle; ;WITH LastAssetEvent AS ( SELECT ae.StoryAssetID, MAX(nso.GlobalSceneIndex) AS LastEventIndex, MAX(ae.SceneID) AS LastEventSceneID, MAX(CONCAT(N'Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle)) AS LastEventSceneLabel FROM dbo.AssetEvents ae INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = ae.SceneID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) GROUP BY ae.StoryAssetID ) SELECT sa.StoryAssetID, sa.AssetName, ak.KindName, sa.Importance, sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, location.LocationPath AS CurrentLocationPath, sa.IsResolved, LastAssetEvent.LastEventSceneID, LastAssetEvent.LastEventSceneLabel, CASE WHEN EXISTS (SELECT 1 FROM dbo.AssetDependencies ad WHERE ad.IsArchived = 0 AND (ad.SourceAssetID = sa.StoryAssetID OR ad.TargetAssetID = sa.StoryAssetID)) THEN 1 ELSE 0 END AS HasDependencies, CASE WHEN EXISTS ( SELECT 1 FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID WHERE cw.ProjectID = @ProjectID AND cw.EntityType IN (N'StoryAsset', N'AssetDependency') AND cw.EntityID IN (sa.StoryAssetID) AND wt.TypeName LIKE N'Asset%' AND cw.IsDismissed = 0 AND cw.IsIntentional = 0 ) THEN 1 ELSE 0 END AS HasDependencyWarnings FROM dbo.StoryAssets sa INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID LEFT JOIN dbo.LocationPaths location ON location.LocationID = sa.CurrentLocationID LEFT JOIN LastAssetEvent ON LastAssetEvent.StoryAssetID = sa.StoryAssetID WHERE sa.ProjectID = @ProjectID AND sa.IsArchived = 0 ORDER BY sa.IsResolved, sa.Importance DESC, sa.AssetName; SELECT ws.SeverityName, COUNT(*) AS WarningCount FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID WHERE cw.ProjectID = @ProjectID AND (@BookID IS NULL OR cw.BookID = @BookID) AND cw.IsDismissed = 0 AND cw.IsIntentional = 0 GROUP BY ws.SeverityName, ws.SortOrder ORDER BY ws.SortOrder DESC; SELECT TOP (12) wt.TypeName AS WarningTypeName, COUNT(*) AS WarningCount FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID WHERE cw.ProjectID = @ProjectID AND (@BookID IS NULL OR cw.BookID = @BookID) AND cw.IsDismissed = 0 AND cw.IsIntentional = 0 GROUP BY wt.TypeName ORDER BY COUNT(*) DESC, wt.TypeName; SELECT TOP (12) cw.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle, COUNT(*) AS WarningCount, MAX(ws.SortOrder) AS HighestSeveritySort FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID INNER JOIN dbo.Scenes s ON s.SceneID = cw.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE cw.ProjectID = @ProjectID AND cw.SceneID IS NOT NULL AND (@BookID IS NULL OR cw.BookID = @BookID) AND cw.IsDismissed = 0 AND cw.IsIntentional = 0 GROUP BY cw.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle ORDER BY COUNT(*) DESC, MAX(ws.SortOrder) DESC; ;WITH ScopeScenes AS ( SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle, COALESCE(s.POVCharacterID, c.POVCharacterID) AS POVCharacterID FROM dbo.NarrativeSceneOrder nso INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = nso.ChapterID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber) AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber) AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber) AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber) ) SELECT COALESCE(ch.CharacterName, N'No POV') AS POVName, ss.POVCharacterID, COUNT(*) AS SceneCount FROM ScopeScenes ss LEFT JOIN dbo.Characters ch ON ch.CharacterID = ss.POVCharacterID GROUP BY ss.POVCharacterID, ch.CharacterName ORDER BY COUNT(*) DESC, POVName; ;WITH ScopeScenes AS ( SELECT nso.SceneID, nso.ChapterID FROM dbo.NarrativeSceneOrder nso WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) ) SELECT c.ChapterID, c.ChapterNumber, c.ChapterTitle, COUNT(DISTINCT COALESCE(s.POVCharacterID, c.POVCharacterID)) AS POVCount FROM dbo.Chapters c INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID WHERE c.ChapterID IN (SELECT ChapterID FROM ScopeScenes) GROUP BY c.ChapterID, c.ChapterNumber, c.ChapterTitle HAVING COUNT(DISTINCT COALESCE(s.POVCharacterID, c.POVCharacterID)) > 1 ORDER BY c.ChapterNumber; SELECT ch.CharacterID, ch.CharacterName, COUNT(DISTINCT sc.SceneID) AS SceneCount FROM dbo.Characters ch LEFT JOIN dbo.SceneCharacters sc ON sc.CharacterID = ch.CharacterID LEFT JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID WHERE ch.ProjectID = @ProjectID AND ch.IsArchived = 0 AND (@BookID IS NULL OR nso.BookID = @BookID OR sc.SceneID IS NULL) GROUP BY ch.CharacterID, ch.CharacterName ORDER BY COUNT(DISTINCT sc.SceneID) DESC, ch.CharacterName; ;WITH SceneCharacterCounts AS ( SELECT nso.SceneID, nso.SceneNumber, nso.SceneTitle, nso.ChapterNumber, COUNT(sc.SceneCharacterID) AS CharacterCount FROM dbo.NarrativeSceneOrder nso LEFT JOIN dbo.SceneCharacters sc ON sc.SceneID = nso.SceneID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) GROUP BY nso.SceneID, nso.SceneNumber, nso.SceneTitle, nso.ChapterNumber ) SELECT TOP (12) * FROM SceneCharacterCounts WHERE CharacterCount >= 5 ORDER BY CharacterCount DESC, ChapterNumber, SceneNumber; ;WITH ScopeScenes AS ( SELECT nso.SceneID, s.PrimaryLocationID FROM dbo.NarrativeSceneOrder nso INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) ) SELECT COALESCE(lp.LocationName, N'No location') AS LocationName, lp.LocationID, COALESCE(lp.LocationPath, N'No location') AS LocationPath, COUNT(*) AS SceneCount FROM ScopeScenes ss LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = ss.PrimaryLocationID GROUP BY lp.LocationID, lp.LocationName, lp.LocationPath ORDER BY COUNT(*) DESC, LocationName; END; GO CREATE OR ALTER PROCEDURE dbo.Analytics_StoryHealth @ProjectID int, @BookID int = NULL, @StartChapterNumber decimal(10,2) = NULL, @EndChapterNumber decimal(10,2) = NULL, @StartSceneNumber decimal(10,2) = NULL, @EndSceneNumber decimal(10,2) = NULL AS BEGIN SET NOCOUNT ON; DECLARE @HasMetricSettings bit = CASE WHEN EXISTS (SELECT 1 FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID) THEN 1 ELSE 0 END; ;WITH ScopeScenes AS ( SELECT nso.* FROM dbo.NarrativeSceneOrder nso WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber) AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber) AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber) AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber) ) SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.ChapterTitle, ss.SceneNumber, ss.SceneTitle, ss.GlobalSceneIndex, ss.BookSceneIndex, smt.MetricTypeID, smt.MetricName, smv.Value, smt.MinValue, smt.MaxValue, CASE WHEN smt.MaxValue = smt.MinValue THEN 0 ELSE CAST(((smv.Value - smt.MinValue) * 100.0) / (smt.MaxValue - smt.MinValue) AS int) END AS [Percent] FROM ScopeScenes ss INNER JOIN dbo.SceneMetricValues smv ON smv.SceneID = ss.SceneID INNER JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID WHERE smt.IsActive = 1 AND ( (@HasMetricSettings = 1 AND EXISTS ( SELECT 1 FROM dbo.ProjectTimelineMetricSettings ptms WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = smt.MetricTypeID )) OR (@HasMetricSettings = 0 AND smt.MetricName IN ( N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Romance', N'Violence', N'Sexual Charge', N'Mystery', N'Hope / Lightness', N'Comedy', N'Darkness' )) ) ORDER BY smt.SortOrder, ss.GlobalSceneIndex; ;WITH ScopeScenes AS ( SELECT nso.SceneID, nso.ChapterID FROM dbo.NarrativeSceneOrder nso WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber) AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber) AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber) AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber) ) SELECT rs.RevisionStatusID, rs.StatusName, COUNT(s.SceneID) AS SceneCount, SUM(CASE WHEN c.ChapterID IS NOT NULL THEN 1 ELSE 0 END) AS ChapterCount FROM dbo.RevisionStatuses rs LEFT JOIN dbo.Scenes s ON s.RevisionStatusID = rs.RevisionStatusID AND s.SceneID IN (SELECT SceneID FROM ScopeScenes) LEFT JOIN dbo.Chapters c ON c.RevisionStatusID = rs.RevisionStatusID AND c.ChapterID IN (SELECT DISTINCT ChapterID FROM ScopeScenes) GROUP BY rs.RevisionStatusID, rs.StatusName, rs.SortOrder ORDER BY rs.SortOrder; ;WITH ScopeScenes AS ( SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle FROM dbo.NarrativeSceneOrder nso WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber) AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber) AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber) AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber) ) SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.SceneNumber, ss.SceneTitle, rs.StatusName AS RevisionStatusName FROM ScopeScenes ss INNER JOIN dbo.Scenes s ON s.SceneID = ss.SceneID INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID WHERE rs.StatusName IN (N'Needs Work', N'Cut Candidate', N'Drafted') ORDER BY rs.SortOrder, ss.BookID, ss.ChapterNumber, ss.SceneNumber; ;WITH ScopeScenes AS ( SELECT nso.SceneID FROM dbo.NarrativeSceneOrder nso WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber) AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber) AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber) AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber) ), Total AS (SELECT COUNT(*) AS TotalScenes FROM ScopeScenes) SELECT spt.ScenePurposeTypeID, spt.PurposeName, COUNT(sp.SceneID) AS SceneCount, CASE WHEN Total.TotalScenes = 0 THEN 0 ELSE CAST(COUNT(sp.SceneID) * 100.0 / Total.TotalScenes AS decimal(5,1)) END AS PercentOfScenes FROM dbo.ScenePurposeTypes spt CROSS JOIN Total LEFT JOIN dbo.ScenePurposes sp ON sp.ScenePurposeTypeID = spt.ScenePurposeTypeID AND sp.SceneID IN (SELECT SceneID FROM ScopeScenes) WHERE spt.IsArchived = 0 GROUP BY spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder, Total.TotalScenes ORDER BY spt.SortOrder; ;WITH ScopeScenes AS ( SELECT nso.SceneID, nso.BookID, nso.BookTitle, nso.ChapterID, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle FROM dbo.NarrativeSceneOrder nso INNER JOIN dbo.Scenes s ON s.SceneID = nso.SceneID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@StartChapterNumber IS NULL OR nso.ChapterNumber >= @StartChapterNumber) AND (@EndChapterNumber IS NULL OR nso.ChapterNumber <= @EndChapterNumber) AND (@StartSceneNumber IS NULL OR nso.SceneNumber >= @StartSceneNumber) AND (@EndSceneNumber IS NULL OR nso.SceneNumber <= @EndSceneNumber) AND ( NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = nso.SceneID) OR ISNULL(NULLIF(LTRIM(RTRIM(s.SceneOutcomeNotes)), N''), N'') = N'' ) ) SELECT ss.SceneID, ss.BookID, ss.BookTitle, ss.ChapterID, ss.ChapterNumber, ss.SceneNumber, ss.SceneTitle, CASE WHEN NOT EXISTS (SELECT 1 FROM dbo.ScenePurposes sp WHERE sp.SceneID = ss.SceneID) THEN 1 ELSE 0 END AS MissingPurpose, CASE WHEN ISNULL(NULLIF(LTRIM(RTRIM(s.SceneOutcomeNotes)), N''), N'') = N'' THEN 1 ELSE 0 END AS MissingOutcome FROM ScopeScenes ss INNER JOIN dbo.Scenes s ON s.SceneID = ss.SceneID ORDER BY ss.BookID, ss.ChapterNumber, ss.SceneNumber; EXEC dbo.Analytics_StoryHealth_Remainder @ProjectID, @BookID, @StartChapterNumber, @EndChapterNumber, @StartSceneNumber, @EndSceneNumber; END; GO