You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Migration006_RecordCompleteness (#55) added a boolean IsComplete + JSON-array NoValueKnown to Quotes/Sources/Characters/People, intended to protect a human-confirmed-complete record from
being silently overwritten by a later import. Verified against the actual current code: this
migration has not shipped in any release tag (v1.7.2 is latest) and isn't merged to main yet —
only present on this feature branch, so nothing here needs to preserve compatibility with a released
database. Also verified: nothing in the codebase currently reads either column to make a decision
— they're written once at insert time (hardcoded 0/'[]') and never consulted again. Quotes.UpdateOnNewestWins excludes them from its SET list, which is the only place they're
referenced at all outside entity property declarations. Conversations/StageDirections/SoundCues
have neither column.
This issue emerged while scoping #162 (Source field decidability): giving Source's Title/Type/ Date a Modify path means, for the first time, the staging engine can attempt to overwrite a field on
a record a human has already reviewed — and that must never happen silently, for the whole batch, not
just the one affected action. This is a general mechanism every future "entity X decidability" issue
needs (Character, Person, Conversation, StageDirection, SoundCue), so it's being built once, generically,
here, rather than reinvented per entity. #162 depends on this issue and only consumes what it builds.
What needs to be done
Replace IsComplete BIT with a 3-state CompletenessStatus enum (Incomplete/NeedsReview/ Complete) on Quotes/Sources/Characters/People, editing Migration006_RecordCompleteness
in place (safe — unshipped). Add both CompletenessStatus and NoValueKnown to Conversations/ StageDirections/SoundCues in the same migration, which have neither column today. Update the
fresh-database baseline schema in the same commit; add a schema-drift test per the existing
baseline/incremental-replay convention.
CompletenessStatus lives in Quotinator.Data.Entities (Data-owned, [SafeValue<TEnum?>]-backed
per ADR 008, registered by the base DatabaseConfiguration) since it's meant to be reused by any
consuming project's entities, not Quotinator-domain-specific — same pattern as ImportActionStatus/ ImportBatchStatus. The migrations that add the columns stay in each table's owning project.
New domain-agnostic CompletenessGuard static class in Quotinator.Data.Import (alongside FieldMergeResolver): ShouldBlock(CompletenessStatus status, IReadOnlySet<string> changedFields)
— returns true only when status == Complete; ComputeNextStatus(CompletenessStatus current, IReadOnlyList<string> noValueKnownAfterApply) — computes the Incomplete → NeedsReview
auto-transition (every field now has a known value, while status was still Incomplete; never
auto-transitions away from Complete).
New ImportActionStatus.Blocked (Data-owned enum, System_ImportActions.Status CHECK constraint
widened via a rebuild-under-temp-name migration per database-conventions.md's Migration004_ImportBatchTypeUserSeed precedent). An entity planner stages Blocked instead of Modify when CompletenessGuard.ShouldBlock returns true for the target row.
ImportActionResolutionCoordinator.TryApplyBatchAsync (Quotinator.Data.Import) widens its
whole-batch guard from Status == Pending to Status is Pending or Blocked — a batch containing
even one unresolved Blocked action holds entirely; no action in it applies, including unrelated,
otherwise-ready actions, until every Blocked action is explicitly decided.
SystemImportAction gains a new nullable, entity-agnostic column: MarkCompletenessAs
(CompletenessStatus?). Deciding any import action, for any entity type, can optionally set the
target record's CompletenessStatus directly (most usefully Complete) as part of that same
decision — always available, not only when resolving a Blocked action. ConflictDecisionRequest
gains a matching shared MarkCompletenessAs property. SqliteImportActionService.DecideAsync → ImportActionResolutionCoordinator.DecideAsync → SystemImportActionWriter.MarkDecidedAsync each
thread the value through to persist it alongside MergedFields/Status → Decided.
ApplyResolvedActionAsync's per-entity branches read action.MarkCompletenessAs back at apply
time: if set, write it directly as the row's CompletenessStatus (explicit human override always
wins, regardless of current status); if not set, fall back to CompletenessGuard.ComputeNextStatus(existingRow.CompletenessStatus, computedNoValueKnown). This
applies to Quote's own existing apply path too, not only to entity types made decidable by later
issues — Quote already has both columns from Schema: record completeness flag and per-field verified-absent markers #55.
DecideAsync/UndoDecisionAsync/TryReverseBatchAsync behaviour for Blocked otherwise matches Pending: deciding a Blocked action is permitted; undo reverts unconditionally to Pending;
reversing a batch containing an unresolved Blocked action is refused until it's resolved (already
true today via the existing Status != Applied guard — no code change needed there).
Background
Migration006_RecordCompleteness(#55) added a booleanIsComplete+ JSON-arrayNoValueKnowntoQuotes/Sources/Characters/People, intended to protect a human-confirmed-complete record frombeing silently overwritten by a later import. Verified against the actual current code: this
migration has not shipped in any release tag (
v1.7.2is latest) and isn't merged tomainyet —only present on this feature branch, so nothing here needs to preserve compatibility with a released
database. Also verified: nothing in the codebase currently reads either column to make a decision
— they're written once at insert time (hardcoded
0/'[]') and never consulted again.Quotes.UpdateOnNewestWinsexcludes them from itsSETlist, which is the only place they'rereferenced at all outside entity property declarations.
Conversations/StageDirections/SoundCueshave neither column.
This issue emerged while scoping #162 (Source field decidability): giving Source's
Title/Type/Datea Modify path means, for the first time, the staging engine can attempt to overwrite a field ona record a human has already reviewed — and that must never happen silently, for the whole batch, not
just the one affected action. This is a general mechanism every future "entity X decidability" issue
needs (Character, Person, Conversation, StageDirection, SoundCue), so it's being built once, generically,
here, rather than reinvented per entity. #162 depends on this issue and only consumes what it builds.
What needs to be done
IsComplete BITwith a 3-stateCompletenessStatusenum (Incomplete/NeedsReview/Complete) onQuotes/Sources/Characters/People, editingMigration006_RecordCompletenessin place (safe — unshipped). Add both
CompletenessStatusandNoValueKnowntoConversations/StageDirections/SoundCuesin the same migration, which have neither column today. Update thefresh-database baseline schema in the same commit; add a schema-drift test per the existing
baseline/incremental-replay convention.
CompletenessStatuslives inQuotinator.Data.Entities(Data-owned,[SafeValue<TEnum?>]-backedper ADR 008, registered by the base
DatabaseConfiguration) since it's meant to be reused by anyconsuming project's entities, not Quotinator-domain-specific — same pattern as
ImportActionStatus/ImportBatchStatus. The migrations that add the columns stay in each table's owning project.CompletenessGuardstatic class inQuotinator.Data.Import(alongsideFieldMergeResolver):ShouldBlock(CompletenessStatus status, IReadOnlySet<string> changedFields)— returns true only when
status == Complete;ComputeNextStatus(CompletenessStatus current, IReadOnlyList<string> noValueKnownAfterApply)— computes theIncomplete → NeedsReviewauto-transition (every field now has a known value, while status was still
Incomplete; neverauto-transitions away from
Complete).ImportActionStatus.Blocked(Data-owned enum,System_ImportActions.StatusCHECK constraintwidened via a rebuild-under-temp-name migration per
database-conventions.md'sMigration004_ImportBatchTypeUserSeedprecedent). An entity planner stagesBlockedinstead ofModifywhenCompletenessGuard.ShouldBlockreturns true for the target row.ImportActionResolutionCoordinator.TryApplyBatchAsync(Quotinator.Data.Import) widens itswhole-batch guard from
Status == PendingtoStatus is Pending or Blocked— a batch containingeven one unresolved
Blockedaction holds entirely; no action in it applies, including unrelated,otherwise-ready actions, until every
Blockedaction is explicitly decided.SystemImportActiongains a new nullable, entity-agnostic column:MarkCompletenessAs(
CompletenessStatus?). Deciding any import action, for any entity type, can optionally set thetarget record's
CompletenessStatusdirectly (most usefullyComplete) as part of that samedecision — always available, not only when resolving a
Blockedaction.ConflictDecisionRequestgains a matching shared
MarkCompletenessAsproperty.SqliteImportActionService.DecideAsync→ImportActionResolutionCoordinator.DecideAsync→SystemImportActionWriter.MarkDecidedAsynceachthread the value through to persist it alongside
MergedFields/Status → Decided.ApplyResolvedActionAsync's per-entity branches readaction.MarkCompletenessAsback at applytime: if set, write it directly as the row's
CompletenessStatus(explicit human override alwayswins, regardless of current status); if not set, fall back to
CompletenessGuard.ComputeNextStatus(existingRow.CompletenessStatus, computedNoValueKnown). Thisapplies to Quote's own existing apply path too, not only to entity types made decidable by later
issues — Quote already has both columns from Schema: record completeness flag and per-field verified-absent markers #55.
DecideAsync/UndoDecisionAsync/TryReverseBatchAsyncbehaviour forBlockedotherwise matchesPending: deciding aBlockedaction is permitted; undo reverts unconditionally toPending;reversing a batch containing an unresolved
Blockedaction is refused until it's resolved (alreadytrue today via the existing
Status != Appliedguard — no code change needed there).Expected tests
Quotinator.Data.TestsCompletenessGuard_ShouldBlock_OnlyTrueWhenStatusIsCompleteQuotinator.Data.TestsCompletenessGuard_ComputeNextStatus_IncompleteToNeedsReviewWhenNoValueKnownEmptyQuotinator.Data.TestsCompletenessGuard_ComputeNextStatus_NeverDemotesFromCompleteQuotinator.Data.TestsMigration_BlockedStatus_CheckConstraintAcceptsNewValueQuotinator.Data.TestsBaseline_And_IncrementalReplay_ProduceIdenticalCompletenessSchemaQuotinator.Data.TestsTryApplyBatchAsync_BlockedActionInBatch_HoldsEntireBatchQuotinator.Data.TestsTryApplyBatchAsync_BlockedActionResolved_UnrelatedActionsThenApplyQuotinator.Engine.TestsApplyBatchAsync_MarkCompletenessAsProvided_OverridesAutoComputeQuotinator.Engine.TestsApplyBatchAsync_MarkCompletenessAsOmitted_FallsBackToAutoComputeQuotinator.Engine.TestsApplyBatchAsync_QuoteAlreadyComplete_MarkCompletenessAsOmitted_StatusUnchangedQuotinator.Api.TestsDecideImportAction_MarkCompletenessAsComplete_PersistsAtApplyDefinition of done