Allow completed character review inspection

This commit is contained in:
Nick Beckley 2026-08-30 22:18:27 +00:00
parent 9c1f9c9182
commit 07113f9b36
3 changed files with 53 additions and 5 deletions

View File

@ -281,6 +281,7 @@ var tests = new (string Name, Action Test)[]
("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls), ("Story Intelligence Review Centre adds no AI calls", StoryIntelligenceReviewCentreAddsNoAiCalls),
("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery), ("Story Intelligence Review Centre uses bounded summary query", StoryIntelligenceReviewCentreUsesBoundedSummaryQuery),
("Story Intelligence detailed review GETs use selected module paths", StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths), ("Story Intelligence detailed review GETs use selected module paths", StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths),
("Story Intelligence completed Character review remains inspectable", StoryIntelligenceCompletedCharacterReviewRemainsInspectable),
("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages), ("Story Intelligence detailed reviews read persisted candidate pages", StoryIntelligenceDetailedReviewsReadPersistedCandidatePages),
("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution), ("Story Intelligence Job 6 uses bounded concurrent chapter execution", StoryIntelligenceJob6UsesBoundedConcurrentChapterExecution),
("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals), ("Story Intelligence global progress uses whole run scene totals", StoryIntelligenceGlobalProgressUsesWholeRunSceneTotals),
@ -1138,6 +1139,41 @@ static void StoryIntelligenceDetailedReviewGetsUseSelectedModulePaths()
Assert(!persistedReviewService.Contains("GetRunAsync", StringComparison.Ordinal), "Persisted review GET service must not traverse old run batches."); Assert(!persistedReviewService.Contains("GetRunAsync", StringComparison.Ordinal), "Persisted review GET service must not traverse old run batches.");
} }
static void StoryIntelligenceCompletedCharacterReviewRemainsInspectable()
{
var root = PlotLineRoot();
var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
var candidateService = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceReviewCandidateService.cs"));
var view = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceCharacters.cshtml"));
var action = ExtractAction(controller, "public async Task<IActionResult> StoryIntelligenceCharacters");
Assert(action.Contains("GetCharacterReviewAsync(batchId, page)", StringComparison.Ordinal), "Character GET should use the selected persisted Character review path.");
Assert(!action.Contains("CharacterReview.Candidates.Count == 0", StringComparison.Ordinal), "Completed empty Character review must not redirect to Review Centre.");
Assert(!action.Contains("CharacterGateSatisfied &&", StringComparison.Ordinal), "Character completion must not be treated as an access blocker.");
Assert(action.TrimEnd().EndsWith("return View(model);\n }", StringComparison.Ordinal), "Character GET should render the Character page when scene analysis is complete.");
Assert(!action.Contains("PrepareCharacterCandidatesAsync", StringComparison.Ordinal), "Character GET must not regenerate candidates.");
Assert(!action.Contains("RecordCharacterImportAsync", StringComparison.Ordinal), "Character GET must not alter CharacterReviewCompletedUtc.");
Assert(!action.Contains("RecordLocationImportAsync", StringComparison.Ordinal), "Character GET must not alter Location completion.");
Assert(!action.Contains("RecordAssetImportAsync", StringComparison.Ordinal), "Character GET must not alter Asset completion.");
Assert(!action.Contains("RecordRelationshipImportAsync", StringComparison.Ordinal), "Character GET must not alter Relationship completion.");
Assert(!action.Contains("RecordKnowledgeImportAsync", StringComparison.Ordinal), "Character GET must not alter Knowledge completion.");
var selectedReview = ExtractBetween(onboarding, "public Task<StoryIntelligenceProgressViewModel?> GetCharacterReviewAsync", "public Task<StoryIntelligenceProgressViewModel?> GetLocationReviewAsync");
Assert(selectedReview.Contains("reviewCandidates.GetCharacterReviewAsync(frame.Batch, pageNumber, pageSize)", StringComparison.Ordinal), "Completed Character page should hydrate from persisted review/canonical data.");
Assert(!selectedReview.Contains("PrepareCharacterCandidatesAsync", StringComparison.Ordinal), "Selected Character GET must not prepare candidates.");
Assert(!selectedReview.Contains("RecordCharacterImportAsync", StringComparison.Ordinal), "Selected Character GET must not mark completion.");
var getReview = ExtractBetween(candidateService, "public async Task<StoryIntelligenceCharacterReviewViewModel> GetCharacterReviewAsync", "public async Task<StoryIntelligenceLocationReviewViewModel> GetLocationReviewAsync");
Assert(getReview.Contains("AliasTargetOptions = existingOptions", StringComparison.Ordinal), "Completed Character page should expose canonical resolved characters for inspection.");
Assert(!getReview.Contains("BuildReviewAsync", StringComparison.Ordinal), "Persisted Character GET must not rebuild candidates.");
Assert(!getReview.Contains("MarkReviewedAsync", StringComparison.Ordinal), "Persisted Character GET must not change decisions.");
Assert(view.Contains("Character Review is complete.", StringComparison.Ordinal), "Completed Character page should state that the review is complete.");
Assert(view.Contains("Resolved character", StringComparison.Ordinal), "Completed Character page should list resolved canonical characters.");
Assert(view.Contains("AliasTargetOptions", StringComparison.Ordinal), "Completed Character page should use canonical Character data when no pending candidates exist.");
}
static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages() static void StoryIntelligenceDetailedReviewsReadPersistedCandidatePages()
{ {
var root = PlotLineRoot(); var root = PlotLineRoot();

View File

@ -339,9 +339,7 @@ public sealed class OnboardingController(
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
} }
return model.CharacterGateSatisfied && model.CharacterReview.Candidates.Count == 0 return View(model);
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
: View(model);
} }
[HttpGet("story-intelligence/locations")] [HttpGet("story-intelligence/locations")]

View File

@ -35,9 +35,23 @@
@if (Model.CharacterReview.Candidates.Count == 0) @if (Model.CharacterReview.Candidates.Count == 0)
{ {
<div class="story-review-note"> <div class="story-review-note">
<strong>No character decisions are waiting.</strong> <strong>Character Review is complete.</strong>
<p class="mb-0">Characters are already up to date, or there were no character suggestions ready to import.</p> <p class="mb-0">@Model.CharacterReview.AliasTargetOptions.Count.ToString("N0") resolved character@(Model.CharacterReview.AliasTargetOptions.Count == 1 ? string.Empty : "s") are available for this book.</p>
</div> </div>
@if (Model.CharacterReview.AliasTargetOptions.Count > 0)
{
<div class="story-character-card-grid">
@foreach (var character in Model.CharacterReview.AliasTargetOptions)
{
<article class="story-character-card">
<div class="story-character-card__body">
<strong>@character.CharacterName</strong>
<p class="mb-0 text-muted">Resolved character</p>
</div>
</article>
}
</div>
}
<div class="onboarding-actions"> <div class="onboarding-actions">
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a> <a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReviewCentre" asp-route-batchId="@Model.BatchID">Back to Review Centre</a>
</div> </div>