Skip to content

Person: explicit id, Modify/decidability, wire up dateOfBirth/dateOfDeath #173

Description

@DutchJaFO

Background

Person has no explicit-id schema section today — it's discovered only implicitly via a Quote's author string field, matched by natural key (name) via ResolvePersonAsync, and is Add-only (EnsurePersonExistsAsync is a plain insert-if-not-exists). Person is already a global entity (no SourceId — unlike Character, one "Winston Churchill" row is already shared across every quote that references him), which makes it the simplest of the remaining entities to give explicit-id/Modify support to: no linkage/scoping design question, unlike Character. This issue also wires up Person.DateOfBirth/DateOfDeath — both columns already exist on the entity (predating this milestone) but nothing anywhere sets them; the Modify plumbing this issue adds makes populating them nearly free, and leaving them permanently dead is its own latent gap otherwise.

This issue's "global entity, Name-keyed" Modify shape is deliberately meant to be the direct template for Character's own explicit-id/Modify issue, once Character's separate per-Source-to-global identity migration (see the linked follow-on issue) lands.

What needs to be done

  1. schemas/source-extended.schema.json gains a people array section + person $def: id (required, UUID v4 pattern, same regex as source/stageDirection/etc.), name (required), dateOfBirth (optional, imprecise ISO 8601 string, same convention as SourceEntry.Date), dateOfDeath (optional, same convention). Purely additive — a file without a people section parses identically to today.
  2. New PersonEntry.cs record in Quotinator.Core.Import, doc-commented like SourceEntry. ParsedSourceFile gains People (defaults []). SourceQuoteFileReader.TryParseExtended gains the new root-key parse.
  3. Sql.People (src/Quotinator.Engine/Queries/Sql.cs) gains SelectExistingById (returns Name, DateOfBirth, DateOfDeath, CompletenessStatus), UpdateFieldsById, SelectCompletenessById, UpdateCompletenessById.
  4. New PlanPeopleAsync (src/Quotinator.Engine/Database/ImportActionPlanner.cs), run before the quote loop, mirroring PlanSourcesAsync's shape: id-match lookup → field-map diff (name, dateOfBirth, dateOfDeath) → unchanged-check (silent reuse) → policy-based resolution → CompletenessGuard.ShouldBlock evaluated against the policy-resolved value → stage Blocked or Modify. Falls back to the existing natural-key lookup (Sql.People.SelectIdByName) when no id match — a not-yet-declared-by-id row found this way stays Add-only/natural-key-matched, same scope boundary as Source's own natural-key fallback. A person discovered only implicitly through a Quote's author string (no explicit people[] entry) stays Add-only forever, same rule.
  5. ApplyResolvedActionAsync's Person case splits on ActionType: Add unchanged; Modify calls the new Sql.People.UpdateFieldsById against MergedFields — the first write path that ever populates DateOfBirth/DateOfDeath.
  6. DecideAsync gains an EntityType == Person && ActionType == Modify branch, mirroring Source's branch shape.
  7. ComputeAmbiguousFields gains a Person case.
  8. ReverseAppliedActionsAsync's Person case splits on ActionType: Add keeps today's soft-delete-if-unreferenced; Modify restores Name/DateOfBirth/DateOfDeath via UpdateFieldsById from ExistingValue.
  9. ClearStaleAddTargetsAsync's Person cleanup branch currently uses the Guid-typed repository path (_personRepository.HardDeleteAsync(Guid.Parse(...))), correct today only because every Person id is EntityIdentity-derived (always uppercase). This issue must switch it to the raw-SQL, case-preserving pattern (RepositorySql.HardDelete("People")), the same fix Source: explicit file-carried id, decoupling matching from Title/Type/Date content #162 made for Source — an explicit people[] id is file-authored and not guaranteed uppercase. Non-optional part of this issue's scope, not a nice-to-have.
  10. ConflictDecisionRequest gains PersonName, PersonDateOfBirth, PersonDateOfDeath (nullable FieldDecision?).

Expected tests

Test class Test method Starts
New: Quotinator.Core.Tests SourceQuoteFileReader_PeopleSection_ParsesCorrectly
New: Quotinator.Engine.Tests PlanPeopleAsync_IdMatchFound_NameDiffers_StagesModifyAction
New: Quotinator.Engine.Tests PlanPeopleAsync_IdMatchFound_NothingChanged_NoActionStaged
New: Quotinator.Engine.Tests PlanPeopleAsync_NoIdMatch_FallsBackToNaturalKey_NoActionStaged
New: Quotinator.Engine.Tests PlanPeopleAsync_CompleteStatus_StagesBlockedNotModify
New: Quotinator.Engine.Tests PlanPeopleAsync_CompleteStatus_SkipPolicy_DoesNotBlock
New: Quotinator.Engine.Tests ApplyBatchAsync_PersonModify_WritesDateOfBirthAndDateOfDeath
New: Quotinator.Engine.Tests DecideAsync_PersonModify_ResolvesFieldDecisions
New: Quotinator.Engine.Tests ReverseBatchAsync_PersonModify_RestoresExistingValue
New: Quotinator.Engine.Tests ClearStaleAddTargetsAsync_PersonExplicitLowercaseId_HardDeletesCorrectly

Definition of done

  • All expected tests listed above start red before implementation
  • All requirements implemented
  • All expected tests pass (green)
  • No regression in related tests
  • Findings summarised in a closing comment

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions