Inherit rolling chronology for cue-free scenes
This commit is contained in:
parent
296a4afb99
commit
10da445713
@ -116,6 +116,13 @@ var tests = new (string Name, Action Test)[]
|
||||
("Story Intelligence temporal rejects unanchored clock time", StoryIntelligenceTemporalRejectsUnanchoredClockTime),
|
||||
("Story Intelligence temporal preserves yearless calendar date clues", StoryIntelligenceTemporalPreservesYearlessCalendarDateClues),
|
||||
("Story Intelligence temporal resolves yearless calendar dates from context", StoryIntelligenceTemporalResolvesYearlessCalendarDatesFromContext),
|
||||
("Story Intelligence temporal inherits previous morning when extraction is empty", StoryIntelligenceTemporalInheritsPreviousMorningWhenExtractionIsEmpty),
|
||||
("Story Intelligence temporal inherits previous evening when extraction is empty", StoryIntelligenceTemporalInheritsPreviousEveningWhenExtractionIsEmpty),
|
||||
("Story Intelligence temporal inherits previous night when extraction is empty", StoryIntelligenceTemporalInheritsPreviousNightWhenExtractionIsEmpty),
|
||||
("Story Intelligence temporal inherits after midnight advancement", StoryIntelligenceTemporalInheritsAfterMidnightAdvancement),
|
||||
("Story Intelligence temporal inherits chapter anchor when extraction is empty", StoryIntelligenceTemporalInheritsChapterAnchorWhenExtractionIsEmpty),
|
||||
("Story Intelligence temporal stays unknown without context or extraction", StoryIntelligenceTemporalStaysUnknownWithoutContextOrExtraction),
|
||||
("Story Intelligence temporal explicit date overrides previous context", StoryIntelligenceTemporalExplicitDateOverridesPreviousContext),
|
||||
("Story Intelligence temporal preserves vague few days", StoryIntelligenceTemporalPreservesVagueFewDays),
|
||||
("Core import applies structural chronology to canonical scenes", CoreImportAppliesStructuralChronologyToCanonicalScenes),
|
||||
("Story Intelligence temporal importer preserves manual chronology", StoryIntelligenceTemporalImporterPreservesManualChronology),
|
||||
@ -7234,6 +7241,73 @@ static void StoryIntelligenceTemporalResolvesYearlessCalendarDatesFromContext()
|
||||
Assert(result.RelativeTimeText == "28 Dec 1982, morning", "Resolved yearless dates should retain the part-of-day label.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalInheritsPreviousMorningWhenExtractionIsEmpty()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 28), new DateTime(1982, 12, 28), "morning");
|
||||
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithoutTemporalExtraction(), context);
|
||||
|
||||
Assert(result.StartDateTime == new DateTime(1982, 12, 28), "Empty extraction should inherit the previous resolved date.");
|
||||
Assert(result.TimeModeName == "Exact Date", "Inherited exact dates should remain exact dates.");
|
||||
Assert(result.RelativeTimeText == "28 Dec 1982, morning", "Empty extraction should inherit the previous part of day.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalInheritsPreviousEveningWhenExtractionIsEmpty()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 28), new DateTime(1982, 12, 28), "evening");
|
||||
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithoutTemporalExtraction(), context);
|
||||
|
||||
Assert(result.StartDateTime == new DateTime(1982, 12, 28), "Empty extraction should inherit the previous evening date.");
|
||||
Assert(result.RelativeTimeText == "28 Dec 1982, evening", "Empty extraction should preserve evening precision.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalInheritsPreviousNightWhenExtractionIsEmpty()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 28), new DateTime(1982, 12, 28), "night");
|
||||
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithoutTemporalExtraction(), context);
|
||||
|
||||
Assert(result.StartDateTime == new DateTime(1982, 12, 28), "Empty extraction should inherit the previous night date.");
|
||||
Assert(result.RelativeTimeText == "28 Dec 1982, night", "Empty extraction should preserve night precision.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalInheritsAfterMidnightAdvancement()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 28), new DateTime(1982, 12, 28), "night");
|
||||
var advanced = StoryIntelligenceTemporalResolver.Resolve(SceneWithTemporal(partOfDay: "night", crossesMidnight: true, evidence: "after midnight", confidence: 0.9m), context);
|
||||
var inherited = StoryIntelligenceTemporalResolver.Resolve(SceneWithoutTemporalExtraction(), advanced.NextContext);
|
||||
|
||||
Assert(advanced.StartDateTime == new DateTime(1982, 12, 29), "After midnight should advance from the previous scene date.");
|
||||
Assert(inherited.StartDateTime == new DateTime(1982, 12, 29), "Empty extraction after midnight should inherit the advanced date.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalInheritsChapterAnchorWhenExtractionIsEmpty()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 28), null, null);
|
||||
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithoutTemporalExtraction(), context);
|
||||
|
||||
Assert(result.StartDateTime == new DateTime(1982, 12, 28), "Empty extraction should inherit chapter anchor when no previous scene chronology exists.");
|
||||
Assert(result.TimeModeName == "Exact Date", "Chapter anchor fallback should produce the anchored date.");
|
||||
Assert(result.RelativeTimeText is null, "Chapter anchor fallback should not invent a part of day.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalStaysUnknownWithoutContextOrExtraction()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(null, null, null);
|
||||
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithoutTemporalExtraction(), context);
|
||||
|
||||
Assert(result.StartDateTime is null, "No extraction and no context should remain unknown.");
|
||||
Assert(result.TimeModeName == "Unknown", "Unknown should be the final fallback when there is no chronology context.");
|
||||
Assert(result.RelativeTimeText is null, "Unknown chronology should not invent display text.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalExplicitDateOverridesPreviousContext()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1982, 12, 28), new DateTime(1982, 12, 28), "night");
|
||||
var result = StoryIntelligenceTemporalResolver.Resolve(SceneWithTemporal(absoluteDate: "29 Dec 1982", partOfDay: "morning", evidence: "29 Dec 1982 morning", confidence: 0.95m), context);
|
||||
|
||||
Assert(result.StartDateTime == new DateTime(1982, 12, 29), "Explicit date should override the previous resolved date.");
|
||||
Assert(result.RelativeTimeText == "29 Dec 1982, morning", "Explicit replacement should keep the current scene part of day.");
|
||||
}
|
||||
|
||||
static void StoryIntelligenceTemporalPreservesVagueFewDays()
|
||||
{
|
||||
var context = new StoryIntelligenceTemporalContext(new DateTime(1983, 10, 4), new DateTime(1983, 10, 4), null);
|
||||
@ -7253,6 +7327,7 @@ static void CoreImportAppliesStructuralChronologyToCanonicalScenes()
|
||||
Assert(onboarding.Contains("await ApplyStructuralChronologyAsync(structuralScenes, result)", StringComparison.Ordinal), "Fresh core import should apply chronology after source text has canonical SceneIDs.");
|
||||
Assert(onboarding.Contains("await ApplyStructuralChronologyAsync(recoveredScenes, existingBuild)", StringComparison.Ordinal), "Recovered core import should apply chronology when missing scene sources are repaired.");
|
||||
Assert(onboarding.Contains("ExtractStructuralTemporalEvidence(structuralScene.SourceText)", StringComparison.Ordinal), "Core import chronology should use SceneManuscriptSources-equivalent structural source text.");
|
||||
Assert(onboarding.Contains("StoryIntelligenceTemporalResolver.Resolve(BuildStructuralTemporalScene(structuralScene), temporalContext)", StringComparison.Ordinal), "Cue-free structural scenes should still enter the rolling resolver so they can inherit chronology.");
|
||||
Assert(onboarding.Contains("HasExistingChronology(canonicalScene)", StringComparison.Ordinal), "Core import chronology should preserve existing author-entered chronology.");
|
||||
Assert(resolver.Contains("HasExplicitYear(clean)", StringComparison.Ordinal), "Temporal resolver should avoid current-year date parsing for yearless manuscript dates.");
|
||||
}
|
||||
@ -7421,6 +7496,15 @@ static SceneIntelligenceScene SceneWithTemporal(
|
||||
}
|
||||
};
|
||||
|
||||
static SceneIntelligenceScene SceneWithoutTemporalExtraction()
|
||||
=> new()
|
||||
{
|
||||
Summary = new SceneIntelligenceSummary { Short = "The scene continues without a new time cue.", Confidence = 0.8m },
|
||||
Setting = new SceneIntelligenceSetting { LocationName = "Pub", LocationType = "interior", Confidence = 0.8m },
|
||||
TemporalAnalysis = null,
|
||||
TimelineClues = []
|
||||
};
|
||||
|
||||
static JsonElement SceneSchemaRoot()
|
||||
=> JsonSerializer.SerializeToElement(StoryIntelligenceSceneStructuredOutputSchema.Contract.Schema);
|
||||
|
||||
|
||||
@ -580,13 +580,7 @@ public sealed class OnboardingService(
|
||||
continue;
|
||||
}
|
||||
|
||||
var temporalScene = BuildStructuralTemporalScene(structuralScene);
|
||||
if (temporalScene is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var resolved = StoryIntelligenceTemporalResolver.Resolve(temporalScene, temporalContext);
|
||||
var resolved = StoryIntelligenceTemporalResolver.Resolve(BuildStructuralTemporalScene(structuralScene), temporalContext);
|
||||
temporalContext = resolved.NextContext;
|
||||
if (string.Equals(resolved.TimeModeName, "Unknown", StringComparison.OrdinalIgnoreCase)
|
||||
|| (!resolved.StartDateTime.HasValue && string.IsNullOrWhiteSpace(resolved.RelativeTimeText)))
|
||||
|
||||
@ -95,6 +95,7 @@ public static class StoryIntelligenceTemporalResolver
|
||||
temporal = InferFromLegacyFields(scene);
|
||||
}
|
||||
|
||||
var hasCurrentTemporalSignal = HasSceneTemporalSignal(temporal, scene);
|
||||
var confidence = NormaliseConfidence(temporal?.Confidence);
|
||||
var contextDate = context.LastDate ?? context.ChapterAnchorDate;
|
||||
var absoluteDate = ParseDate(temporal?.AbsoluteDate, contextDate) ?? ParseDate(fallbackText, contextDate);
|
||||
@ -103,12 +104,19 @@ public static class StoryIntelligenceTemporalResolver
|
||||
var isDateDeterministic = absoluteDate.HasValue;
|
||||
var relativeText = FirstNonBlank(temporal?.Evidence, temporal?.AbsoluteDate, scene?.Setting?.DateOrTimeReference, scene?.Setting?.TimeOfDay);
|
||||
var currentPartOfDay = CleanPartOfDay(temporal?.PartOfDay ?? scene?.Setting?.TimeOfDay ?? fallbackText);
|
||||
var inheritedPartOfDay = !hasCurrentTemporalSignal && context.LastDate.HasValue ? context.LastPartOfDay : null;
|
||||
var displayPartOfDay = currentPartOfDay ?? inheritedPartOfDay;
|
||||
var partOfDayForTime = currentPartOfDay ?? context.LastPartOfDay;
|
||||
var exactTime = ParseTime(FirstNonBlank(temporal?.ExactTime, temporal?.Evidence, scene?.Setting?.DateOrTimeReference), partOfDayForTime);
|
||||
|
||||
if (!resolvedDate.HasValue && baseDate.HasValue)
|
||||
{
|
||||
if (temporal?.CrossesMidnight == true)
|
||||
if (!hasCurrentTemporalSignal)
|
||||
{
|
||||
resolvedDate = baseDate.Value.Date;
|
||||
isDateDeterministic = true;
|
||||
}
|
||||
else if (temporal?.CrossesMidnight == true)
|
||||
{
|
||||
resolvedDate = baseDate.Value.AddDays(1);
|
||||
isDateDeterministic = true;
|
||||
@ -128,7 +136,7 @@ public static class StoryIntelligenceTemporalResolver
|
||||
resolvedDate = baseDate.Value.Date;
|
||||
isDateDeterministic = true;
|
||||
}
|
||||
else if (!HasApproximateSignal(temporal, fallbackText) && (currentPartOfDay is not null || HasSceneTemporalSignal(temporal, scene)))
|
||||
else if (!HasApproximateSignal(temporal, fallbackText) && (currentPartOfDay is not null || hasCurrentTemporalSignal))
|
||||
{
|
||||
resolvedDate = baseDate.Value.Date;
|
||||
isDateDeterministic = true;
|
||||
@ -162,12 +170,12 @@ public static class StoryIntelligenceTemporalResolver
|
||||
|
||||
var displayText = string.Equals(mode, "Unknown", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: BuildRelativeText(start, exactTime, currentPartOfDay, relativeText, temporal);
|
||||
: BuildRelativeText(start, exactTime, displayPartOfDay, relativeText, temporal);
|
||||
var resolvedConfidence = ResolveConfidenceName(confidence, isDateDeterministic, mode);
|
||||
var nextContext = context with
|
||||
{
|
||||
LastDate = start?.Date ?? (isDateDeterministic ? resolvedDate : context.LastDate),
|
||||
LastPartOfDay = currentPartOfDay ?? context.LastPartOfDay
|
||||
LastPartOfDay = displayPartOfDay ?? context.LastPartOfDay
|
||||
};
|
||||
|
||||
return new StoryIntelligenceTemporalResolution(start, null, null, null, displayText, mode, resolvedConfidence, nextContext);
|
||||
@ -518,8 +526,7 @@ public static class StoryIntelligenceTemporalResolver
|
||||
|
||||
private static bool HasSceneTemporalSignal(SceneIntelligenceTemporalAnalysis? temporal, SceneIntelligenceScene? scene)
|
||||
=> temporal is not null
|
||||
&& (temporal.Confidence > 0
|
||||
|| !string.IsNullOrWhiteSpace(temporal.AbsoluteDate)
|
||||
&& (!string.IsNullOrWhiteSpace(temporal.AbsoluteDate)
|
||||
|| !string.IsNullOrWhiteSpace(temporal.ExactTime)
|
||||
|| !string.IsNullOrWhiteSpace(temporal.Evidence)
|
||||
|| !string.IsNullOrWhiteSpace(scene?.Setting?.DateOrTimeReference)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user