Skip to content

A case-only change to a display-bearing field cannot be reviewed or corrected #437

Description

@DutchJaFO

Description

ADR 024
states that a content-equality comparison, one whose answer decides whether a value is unchanged
rather than whether it matches, is case-sensitive for every display-bearing field of every entity.
The codebase does not satisfy Decisions 3, 5 and 6, and the ADR's Known non-compliance section
points at this issue as the one that closes the gap.

Three distinct failures, measured against a container built from this working tree:

  1. A case-only change on the incoming side is never reported stale. ConflictRuleLookup calls the
    two-argument FieldMergeResolver.ValuesEqual, which cannot see which field it is comparing, in all
    three of its comparisons (ConflictRuleLookup.cs:68, :73, :84).
  2. A rule cannot write a case-only correction. A rule whose wanted value differs from the stored
    value only by case reports already-applied and writes nothing, so correcting casing through a rule
    is impossible, which is the opposite of what the rule mechanism exists for.
  3. A case-only difference stages an action with nothing to decide. The Modify action's
    ambiguousFields is empty, so a reviewer sees an item carrying no decidable field and the casing
    is never corrected by any route. Same shape as A quote held for review over a case-only text change shows nothing to decide, and is decided without asking #409, reached through the rule path rather than the
    staging path.

Beyond the rule path, FieldMergeResolver.ValuesEqual's two-argument overload folds case for every
entity's conflict and merge detection, and QuoteFieldMerge.CaseSensitiveContentFields covers a
quote's quoteText and character only. Every other display-bearing field on every entity is
therefore non-compliant too: a Source title, a Series, Universe, Season or Person name, and a quote's
own author.

Two things this issue must determine rather than assume. Why the action is classified Stale
rather than Pending, given that staleness is judged on the incoming side alone and the incoming side
had not moved; and whether a surviving Stale row suppresses re-staging of the same entity on a later
reseed, which is what made one attempted control run inconclusive.

Explicitly out of scope: identity and lookup comparisons, which ADR 024's Decisions 1 and 2 keep
case-insensitive deliberately. A case-only retitling must continue not to re-identify an entity, and a
natural-key lookup must continue to match across casing. Changing a title's punctuation is also out of
scope: it changes the normalised form the derived id is built from, so it produces a different entity
rather than a changed one.

Reproduction steps

Unit level, against Quotinator.Data.dll, constructing a ConflictRuleLookup with one rule governing
quoteText whose recordedIncomingValue is "Hello there." and calling TryResolve with a current
incoming value of "HELLO THERE.": the outcome is AlreadyApplied. With a genuinely different
incoming text it is Stale, so the mechanism works and only case is folded away.

Live, on a Fresh container seeded from the bundled sources:

dotnet script scripts/testing/test-env.csx -- create --name qt-case --port 18690
docker stop -t 15 qt-case
docker cp qt-case:/data/quotinatordata.db .claude/temp/case-test.db
dotnet script scripts/testing/execute-sql.csx -- --db ".claude/temp/case-test.db" `
  --sql "UPDATE Quotinator_Character SET Name = 'FERNANDO VERA' WHERE Id = '00457e1e-1e03-7849-828b-371149675434'"
docker cp .claude/temp/case-test.db qt-case:/data/quotinatordata.db
docker start qt-case
dotnet script scripts/testing/http.csx -- --url "http://localhost:18690/api/v1/health" --wait-for 200 --status
dotnet script scripts/testing/http.csx -- --url "http://localhost:18690/api/v1/admin/database/reseed" `
  --method POST --header "X-Api-Key: smoketest" --expect 200 --status
(Invoke-RestMethod "http://localhost:18690/api/v1/quotes/e69951f1-4d01-964d-86d5-13f80f5bfd8a").character
(Invoke-RestMethod "http://localhost:18690/api/v1/import/actions?status=stale&pageSize=0").items[0] |
  ConvertTo-Json -Depth 8

Expected behaviour

ADR 024's Decisions 3, 5 and 6. A content comparison takes the field's classification, so a case-only
change to a display-bearing field is seen; the action staged for it names that field among its
decidable fields; and a rule whose wanted value differs from the stored value only by case is applied.

Actual behaviour

The reseed leaves character as FERNANDO VERA. The staged action is:

actionType      Modify
status          Stale
entityId        e69951f1-4d01-964d-86d5-13f80f5bfd8a
existingFields  character = "FERNANDO VERA"
incomingFields  character = "Fernando Vera"
ambiguousFields []

Failing tests

Test class Test method Status before fix
ConflictRuleLookupTests TryResolve_CaseOnlyIncomingChangeOnAContentField_ReportsStale ❌
ConflictRuleLookupTests TryResolve_WantedValueDiffersFromStoredByCaseOnly_ReportsApply ❌
ConflictRuleLookupTests TryResolve_CaseOnlyChangeOnAClosedSetValue_IsNotStale ❌
FieldMergeResolverTests ValuesEqual_CaseOnlyDifferenceOnADisplayBearingField_IsNotEqual ❌
ImportActionPlannerTests Plan_CaseOnlyDifferenceOnASourceTitle_StagesWithThatFieldDecidable ❌
ImportActionPlannerTests Plan_CaseOnlyDifferenceOnACharacterName_StagesWithThatFieldDecidable ❌
(live) A new docs/automated-testing/identity-and-casing/ document for the reproduction above ❌

Definition of done

  • Failing test(s) listed above are red before the fix is written
  • Fix implemented
  • All listed tests pass (green)
  • No regression in related tests
  • Findings summarised in a closing comment

Activity

  1. added this to the v1.9.0 milestone on Oct 6, 2026
  2. added 3 commits that reference this issue on Oct 6, 2026
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

    bugSomething isn't working

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions