SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO MERGE dbo.WarningTypes AS target USING (VALUES (N'Knowledge Continuity', N'Character may act on information before a recorded knowledge event.', 220), (N'Abrupt Relationship Change', N'Relationship intensity changes sharply between nearby scenes.', 230), (N'Long Character Absence', N'Character disappears for a long stretch of narrative scenes.', 240), (N'Character Underused', N'Character appears lightly compared with their story connections.', 250), (N'Scene Character Overload', N'Scene contains a large number of character appearances.', 260) ) AS source (TypeName, Description, SortOrder) ON target.TypeName = source.TypeName WHEN MATCHED THEN UPDATE SET Description = source.Description, SortOrder = source.SortOrder, IsActive = 1 WHEN NOT MATCHED THEN INSERT (TypeName, Description, SortOrder, IsActive) VALUES (source.TypeName, source.Description, source.SortOrder, 1); GO CREATE OR ALTER PROCEDURE dbo.Dynamics_CharacterArc @CharacterID int AS BEGIN SET NOCOUNT ON; DECLARE @ProjectID int = (SELECT ProjectID FROM dbo.Characters WHERE CharacterID = @CharacterID); SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived FROM dbo.Characters WHERE CharacterID = @CharacterID; SELECT sc.SceneID, nso.BookTitle, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle, nso.GlobalSceneIndex, CAST(CASE WHEN COALESCE(s.POVCharacterID, c.POVCharacterID) = @CharacterID THEN 1 ELSE 0 END AS bit) AS IsPov, MAX(CASE WHEN smt.MetricName = N'Emotional Weight' THEN smv.Value END) AS EmotionalWeight, MAX(CASE WHEN smt.MetricName = N'Tension' THEN smv.Value END) AS Tension, MAX(CASE WHEN smt.MetricName = N'Romance' THEN smv.Value END) AS Romance, MAX(CASE WHEN smt.MetricName = N'Action' THEN smv.Value END) AS Action, MAX(CASE WHEN smt.MetricName = N'Darkness' THEN smv.Value END) AS Darkness, MAX(CASE WHEN smt.MetricName = N'Hope / Lightness' THEN smv.Value END) AS HopeLightness, MAX(CASE WHEN smt.MetricName = N'Overall Intensity' THEN smv.Value END) AS OverallIntensity, role.TypeName AS RoleInSceneTypeName, presence.TypeName AS PresenceTypeName, sc.EmotionalState, sc.AppearanceNotes, notes.KeyNotes FROM dbo.SceneCharacters sc INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID LEFT JOIN dbo.SceneMetricValues smv ON smv.SceneID = sc.SceneID LEFT JOIN dbo.SceneMetricTypes smt ON smt.MetricTypeID = smv.MetricTypeID OUTER APPLY ( SELECT STRING_AGG(CONVERT(nvarchar(max), COALESCE(sn.NoteTitle, sn.NoteText)), N' | ') AS KeyNotes FROM dbo.SceneNotes sn INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID WHERE sn.SceneID = sc.SceneID AND sn.IsResolved = 0 AND snt.TypeName IN (N'Emotional Intent', N'Character Reminder', N'Revision Note', N'TODO') ) notes WHERE sc.CharacterID = @CharacterID GROUP BY sc.SceneID, nso.BookTitle, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle, nso.GlobalSceneIndex, s.POVCharacterID, c.POVCharacterID, role.TypeName, presence.TypeName, sc.EmotionalState, sc.AppearanceNotes, notes.KeyNotes ORDER BY nso.GlobalSceneIndex; EXEC dbo.RelationshipEvent_ListByCharacter @CharacterID; EXEC dbo.CharacterKnowledge_ListByCharacter @CharacterID; SELECT N'Character Presence' AS Category, ws.SeverityName, cw.Message AS Title, cw.Details AS Detail, cw.SceneID, cw.EntityID, cw.EntityType FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID WHERE cw.ProjectID = @ProjectID AND cw.IsDismissed = 0 AND ( (cw.EntityType = N'Character' AND cw.EntityID = @CharacterID) OR cw.SceneID IN (SELECT SceneID FROM dbo.SceneCharacters WHERE CharacterID = @CharacterID) ) AND wt.TypeName IN (N'Knowledge Continuity', N'Long Character Absence', N'Character Underused', N'Scene Character Overload'); END; GO CREATE OR ALTER PROCEDURE dbo.Dynamics_RelationshipEvolution @CharacterRelationshipID int AS BEGIN SET NOCOUNT ON; SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName, cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName, cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes, cr.CreatedDate, cr.UpdatedDate, cr.IsArchived FROM dbo.CharacterRelationships cr INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID WHERE cr.CharacterRelationshipID = @CharacterRelationshipID; SELECT re.RelationshipEventID, re.CharacterRelationshipID, re.SceneID, nso.BookTitle, nso.ChapterNumber, nso.SceneNumber, nso.SceneTitle, nso.GlobalSceneIndex, rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter, re.Description FROM dbo.RelationshipEvents re INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = re.SceneID WHERE re.CharacterRelationshipID = @CharacterRelationshipID ORDER BY nso.GlobalSceneIndex, re.RelationshipEventID; SELECT N'Relationship Continuity' AS Category, ws.SeverityName, cw.Message AS Title, cw.Details AS Detail, cw.SceneID, cw.EntityID, cw.EntityType FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID WHERE cw.IsDismissed = 0 AND cw.EntityType = N'Relationship' AND cw.EntityID = @CharacterRelationshipID AND wt.TypeName = N'Abrupt Relationship Change'; END; GO CREATE OR ALTER PROCEDURE dbo.Dynamics_KnowledgeTimeline @CharacterID int AS BEGIN SET NOCOUNT ON; DECLARE @ProjectID int = (SELECT ProjectID FROM dbo.Characters WHERE CharacterID = @CharacterID); SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived FROM dbo.Characters WHERE CharacterID = @CharacterID; EXEC dbo.CharacterKnowledge_ListByCharacter @CharacterID; SELECT N'Knowledge Continuity' AS Category, ws.SeverityName, cw.Message AS Title, cw.Details AS Detail, cw.SceneID, cw.EntityID, cw.EntityType FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID WHERE cw.ProjectID = @ProjectID AND cw.IsDismissed = 0 AND cw.EntityType = N'Character' AND cw.EntityID = @CharacterID AND wt.TypeName = N'Knowledge Continuity'; END; GO CREATE OR ALTER PROCEDURE dbo.Dynamics_StoryDashboard @ProjectID int AS BEGIN SET NOCOUNT ON; ;WITH Pairings AS ( SELECT a.CharacterID AS CharacterAID, ca.CharacterName AS CharacterAName, b.CharacterID AS CharacterBID, cb.CharacterName AS CharacterBName, a.SceneID, nso.GlobalSceneIndex, CONCAT(nso.BookTitle, N' / Ch ', nso.ChapterNumber, N' / Scene ', nso.SceneNumber, N': ', nso.SceneTitle) AS SceneLabel FROM dbo.SceneCharacters a INNER JOIN dbo.SceneCharacters b ON b.SceneID = a.SceneID AND b.CharacterID > a.CharacterID INNER JOIN dbo.Characters ca ON ca.CharacterID = a.CharacterID INNER JOIN dbo.Characters cb ON cb.CharacterID = b.CharacterID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = a.SceneID WHERE ca.ProjectID = @ProjectID AND cb.ProjectID = @ProjectID AND ca.IsArchived = 0 AND cb.IsArchived = 0 ), Ranked AS ( SELECT CharacterAID, CharacterAName, CharacterBID, CharacterBName, COUNT(*) AS SharedSceneCount, MIN(GlobalSceneIndex) AS FirstIndex, MAX(GlobalSceneIndex) AS LastIndex FROM Pairings GROUP BY CharacterAID, CharacterAName, CharacterBID, CharacterBName ) SELECT TOP (40) r.CharacterAID, r.CharacterAName, r.CharacterBID, r.CharacterBName, r.SharedSceneCount, firstScene.SceneLabel AS FirstSceneLabel, lastScene.SceneLabel AS LastSceneLabel FROM Ranked r OUTER APPLY (SELECT TOP (1) SceneLabel FROM Pairings p WHERE p.CharacterAID = r.CharacterAID AND p.CharacterBID = r.CharacterBID ORDER BY p.GlobalSceneIndex) firstScene OUTER APPLY (SELECT TOP (1) SceneLabel FROM Pairings p WHERE p.CharacterAID = r.CharacterAID AND p.CharacterBID = r.CharacterBID ORDER BY p.GlobalSceneIndex DESC) lastScene ORDER BY r.SharedSceneCount DESC, r.CharacterAName, r.CharacterBName; SELECT Category, SeverityName, Title, Detail, SceneID, EntityID, EntityType FROM ( SELECT N'Continuity' AS Category, ws.SeverityName, cw.Message AS Title, cw.Details AS Detail, cw.SceneID, cw.EntityID, cw.EntityType FROM dbo.ContinuityWarnings cw INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID WHERE cw.ProjectID = @ProjectID AND cw.IsDismissed = 0 AND wt.TypeName IN (N'Knowledge Continuity', N'Abrupt Relationship Change', N'Long Character Absence', N'Character Underused', N'Scene Character Overload') ) issues ORDER BY SeverityName DESC, Title; END; GO CREATE OR ALTER PROCEDURE dbo.Dynamics_RunContinuityChecks @ProjectID int, @BookID int = NULL, @SceneID int = NULL AS BEGIN SET NOCOUNT ON; ;WITH RelationshipSteps AS ( SELECT cr.CharacterRelationshipID, cr.ProjectID, nso.BookID, nso.ChapterID, re.SceneID, ca.CharacterName AS CharacterAName, cb.CharacterName AS CharacterBName, re.Intensity, LAG(re.Intensity) OVER (PARTITION BY cr.CharacterRelationshipID ORDER BY nso.GlobalSceneIndex, re.RelationshipEventID) AS PreviousIntensity, LAG(nso.SceneTitle) OVER (PARTITION BY cr.CharacterRelationshipID ORDER BY nso.GlobalSceneIndex, re.RelationshipEventID) AS PreviousSceneTitle FROM dbo.RelationshipEvents re INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID AND cr.IsArchived = 0 INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = re.SceneID WHERE cr.ProjectID = @ProjectID ) INSERT dbo.ContinuityWarnings (ProjectID, BookID, ChapterID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details) SELECT rs.ProjectID, rs.BookID, rs.ChapterID, rs.SceneID, N'Relationship', rs.CharacterRelationshipID, wt.WarningTypeID, ws.WarningSeverityID, N'Possible emotional transition issue. Relationship intensity changes sharply between scenes.', CONCAT(rs.CharacterAName, N' / ', rs.CharacterBName, N' changes from ', rs.PreviousIntensity, N' to ', rs.Intensity, N'. Previous scene: ', rs.PreviousSceneTitle, N'.') FROM RelationshipSteps rs CROSS JOIN dbo.WarningTypes wt CROSS JOIN dbo.WarningSeverities ws WHERE wt.TypeName = N'Abrupt Relationship Change' AND ws.SeverityName = N'Info' AND rs.Intensity IS NOT NULL AND rs.PreviousIntensity IS NOT NULL AND ABS(rs.Intensity - rs.PreviousIntensity) >= 5 AND (@BookID IS NULL OR rs.BookID = @BookID) AND (@SceneID IS NULL OR rs.SceneID = @SceneID) AND NOT EXISTS (SELECT 1 FROM dbo.ContinuityWarnings existing WHERE existing.ProjectID = rs.ProjectID AND existing.SceneID = rs.SceneID AND existing.EntityType = N'Relationship' AND existing.EntityID = rs.CharacterRelationshipID AND existing.WarningTypeID = wt.WarningTypeID AND existing.IsDismissed = 0 AND existing.IsIntentional = 0); ;WITH Appearances AS ( SELECT sc.CharacterID, ch.CharacterName, nso.ProjectID, nso.BookID, nso.ChapterID, sc.SceneID, nso.GlobalSceneIndex, LAG(nso.GlobalSceneIndex) OVER (PARTITION BY sc.CharacterID ORDER BY nso.GlobalSceneIndex) AS PreviousIndex FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID WHERE ch.ProjectID = @ProjectID AND ch.IsArchived = 0 ) INSERT dbo.ContinuityWarnings (ProjectID, BookID, ChapterID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details) SELECT ProjectID, BookID, ChapterID, SceneID, N'Character', CharacterID, wt.WarningTypeID, ws.WarningSeverityID, CONCAT(N'Possible review: ', CharacterName, N' is absent for ', GlobalSceneIndex - PreviousIndex - 1, N' narrative scenes.'), N'Long absences are sometimes useful, but this may be worth checking after a major beat.' FROM Appearances CROSS JOIN dbo.WarningTypes wt CROSS JOIN dbo.WarningSeverities ws WHERE wt.TypeName = N'Long Character Absence' AND ws.SeverityName = N'Info' AND PreviousIndex IS NOT NULL AND GlobalSceneIndex - PreviousIndex - 1 >= 12 AND (@BookID IS NULL OR BookID = @BookID) AND (@SceneID IS NULL OR SceneID = @SceneID) AND NOT EXISTS (SELECT 1 FROM dbo.ContinuityWarnings existing WHERE existing.ProjectID = Appearances.ProjectID AND existing.SceneID = Appearances.SceneID AND existing.EntityType = N'Character' AND existing.EntityID = Appearances.CharacterID AND existing.WarningTypeID = wt.WarningTypeID AND existing.IsDismissed = 0 AND existing.IsIntentional = 0); ;WITH CharacterUsage AS ( SELECT ch.ProjectID, ch.CharacterID, ch.CharacterName, COUNT(DISTINCT sc.SceneID) AS AppearanceCount, COUNT(DISTINCT cr.CharacterRelationshipID) AS RelationshipCount FROM dbo.Characters ch LEFT JOIN dbo.SceneCharacters sc ON sc.CharacterID = ch.CharacterID LEFT JOIN dbo.CharacterRelationships cr ON cr.ProjectID = ch.ProjectID AND cr.IsArchived = 0 AND ch.CharacterID IN (cr.CharacterAID, cr.CharacterBID) WHERE ch.ProjectID = @ProjectID AND ch.IsArchived = 0 AND @SceneID IS NULL GROUP BY ch.ProjectID, ch.CharacterID, ch.CharacterName HAVING COUNT(DISTINCT cr.CharacterRelationshipID) >= 2 AND COUNT(DISTINCT sc.SceneID) <= 1 ) INSERT dbo.ContinuityWarnings (ProjectID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details) SELECT ProjectID, N'Character', CharacterID, wt.WarningTypeID, ws.WarningSeverityID, CONCAT(N'Possible review: ', CharacterName, N' has several relationship links but very few scene appearances.'), N'This may be intentional, but the character could be underused relative to their story connections.' FROM CharacterUsage CROSS JOIN dbo.WarningTypes wt CROSS JOIN dbo.WarningSeverities ws WHERE wt.TypeName = N'Character Underused' AND ws.SeverityName = N'Info' AND NOT EXISTS (SELECT 1 FROM dbo.ContinuityWarnings existing WHERE existing.ProjectID = CharacterUsage.ProjectID AND existing.EntityType = N'Character' AND existing.EntityID = CharacterUsage.CharacterID AND existing.WarningTypeID = wt.WarningTypeID AND existing.IsDismissed = 0 AND existing.IsIntentional = 0); ;WITH SceneCounts AS ( SELECT nso.ProjectID, nso.BookID, nso.ChapterID, nso.SceneID, nso.SceneTitle, COUNT(*) AS CharacterCount FROM dbo.SceneCharacters sc INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID WHERE nso.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@SceneID IS NULL OR nso.SceneID = @SceneID) GROUP BY nso.ProjectID, nso.BookID, nso.ChapterID, nso.SceneID, nso.SceneTitle HAVING COUNT(*) >= 7 ) INSERT dbo.ContinuityWarnings (ProjectID, BookID, ChapterID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details) SELECT ProjectID, BookID, ChapterID, SceneID, N'Scene', SceneID, wt.WarningTypeID, ws.WarningSeverityID, CONCAT(N'Possible review: this scene has ', CharacterCount, N' character appearances.'), N'Busy scenes can work well, but may need extra clarity around focus and blocking.' FROM SceneCounts CROSS JOIN dbo.WarningTypes wt CROSS JOIN dbo.WarningSeverities ws WHERE wt.TypeName = N'Scene Character Overload' AND ws.SeverityName = N'Info' AND NOT EXISTS (SELECT 1 FROM dbo.ContinuityWarnings existing WHERE existing.ProjectID = SceneCounts.ProjectID AND existing.SceneID = SceneCounts.SceneID AND existing.EntityType = N'Scene' AND existing.EntityID = SceneCounts.SceneID AND existing.WarningTypeID = wt.WarningTypeID AND existing.IsDismissed = 0 AND existing.IsIntentional = 0); ;WITH Knowledge AS ( SELECT ck.CharacterID, ch.CharacterName, ck.StoryAssetID, ck.PlotThreadID, ck.SceneID, nso.ProjectID, nso.BookID, nso.ChapterID, nso.GlobalSceneIndex, MIN(nso.GlobalSceneIndex) OVER (PARTITION BY ck.CharacterID, ISNULL(ck.StoryAssetID, 0), ISNULL(ck.PlotThreadID, 0)) AS FirstKnownIndex FROM dbo.CharacterKnowledge ck INNER JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = ck.SceneID WHERE ch.ProjectID = @ProjectID ), Candidate AS ( SELECT DISTINCT sc.CharacterID, ch.CharacterName, te.PlotThreadID, ae.StoryAssetID, nso.ProjectID, nso.BookID, nso.ChapterID, sc.SceneID, nso.GlobalSceneIndex FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID INNER JOIN dbo.NarrativeSceneOrder nso ON nso.SceneID = sc.SceneID LEFT JOIN dbo.ThreadEvents te ON te.SceneID = sc.SceneID LEFT JOIN dbo.AssetEvents ae ON ae.SceneID = sc.SceneID WHERE ch.ProjectID = @ProjectID AND (@BookID IS NULL OR nso.BookID = @BookID) AND (@SceneID IS NULL OR sc.SceneID = @SceneID) AND (te.PlotThreadID IS NOT NULL OR ae.StoryAssetID IS NOT NULL) ) INSERT dbo.ContinuityWarnings (ProjectID, BookID, ChapterID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID, Message, Details) SELECT c.ProjectID, c.BookID, c.ChapterID, c.SceneID, N'Character', c.CharacterID, wt.WarningTypeID, ws.WarningSeverityID, CONCAT(N'Possible review: ', c.CharacterName, N' appears in a scene involving information without an earlier knowledge event.'), N'This is advisory only. Add a knowledge event if the character is meant to know this, or mark the warning intentional.' FROM Candidate c CROSS JOIN dbo.WarningTypes wt CROSS JOIN dbo.WarningSeverities ws WHERE wt.TypeName = N'Knowledge Continuity' AND ws.SeverityName = N'Info' AND NOT EXISTS ( SELECT 1 FROM Knowledge k WHERE k.CharacterID = c.CharacterID AND ((c.StoryAssetID IS NOT NULL AND k.StoryAssetID = c.StoryAssetID) OR (c.PlotThreadID IS NOT NULL AND k.PlotThreadID = c.PlotThreadID)) AND k.FirstKnownIndex <= c.GlobalSceneIndex ) AND NOT EXISTS (SELECT 1 FROM dbo.ContinuityWarnings existing WHERE existing.ProjectID = c.ProjectID AND existing.SceneID = c.SceneID AND existing.EntityType = N'Character' AND existing.EntityID = c.CharacterID AND existing.WarningTypeID = wt.WarningTypeID AND existing.IsDismissed = 0 AND existing.IsIntentional = 0); END; GO