A change between two loosely equal values is recorded, and rendered, as no change
## Problem
Two comparisons in this project decide whether a value changed, and both were loose.
### The no-op gate drops the row
`AuditTrailEntityHooks::isNoOpUpdate()` states the bar in its own docblock: *"Whether the save left every field **byte-identical** pre / post."* It compared two digests with `==`. PHP's loose array comparison compares the values loosely, so two numeric strings are compared as numbers: `"01" == "1"`, `"1e2" == "100"`, `"0.50" == "0.5"`.
With `skip_no_op_updates` enabled on a bundle, editing any numeric-looking text value from `01` to `1` was classified as a no-op and no audit row was written at all. A completeness module dropped a real change, silently.
### The delta hides it on the page
`SnapshotDelta::computeStateWithDelta()` decides which fields go into `delta.original`, and used `!=` for the same question. So where a row IS written, the same edit landed with the new value in `state` and no entry in `delta.original`: the entry detail page filed the field under *Show N unchanged fields* and showed `1` as though it had always been there.
### Why `===` is not the fix
The loose comparison is there for a reason, and `SnapshotDeltaTest::testScalarTypeDriftIsUnchanged()` pins it: Drupal field storage routinely surfaces one logical value with a different scalar type across a read/write round trip, so a decimal field holds `10000` on the saved handle and `"10000.00"` on the freshly loaded original with nothing having changed. A strict comparison calls that a change and writes a row on every save of every entity with a typed column.
## Reproduction
Kernel probe on `entity_test` with `skip_no_op_updates: TRUE`:
```
rows after create ('01') 1
rows after '01' -> '1' 1 (expected 2)
rows after '1' -> 'two' 2
```
## Fix
The types separate the two cases, and nothing else does. Where **both sides are strings** the storage layer retyped nothing, so a byte difference is a content difference; anything else is the drift the loose test exists for and stays loosely compared. Arrays are walked, so a multi-delta field is decided item by item rather than by one loose comparison of the whole list.
`SnapshotDelta::valuesDiffer()` is the one owner of that rule. The delta the row carries and the check that decides whether to write a row both read it, so they cannot disagree about what the same value is.
## Test coverage
Each confirmed to fail against the unfixed code and to pass with the change:
- `AuditTrailEntityHooksTest::testNumericLookingChangeIsRecorded` — `01` to `1` writes a row, and a genuine re-save still writes none.
- `SnapshotDeltaTest::testNumericLookingStringsThatDifferAreRecorded` — the three pairs above reach `delta.original`.
- `SnapshotDeltaTest::testMultiDeltaListIsComparedItemByItem` — one loose comparison of a whole list would have put the first case back as soon as the field held two values.
Two controls, which pass both ways by design and are what say the tightening did not buy the fix at the cost of a row on every save: `AuditTrailEntityHooksTest::testTypeDriftIsStillTreatedAsNoOp`, and the existing `SnapshotDeltaTest::testScalarTypeDriftIsUnchanged`, which is the decision this fix had to keep.
---
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the fix and its tests. I reviewed both, and the new tests were confirmed to fail against the unfixed code and to pass with the change.)
issue
GitLab AI Context
Project: project/audit_trail
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/audit_trail/-/raw/1.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/audit_trail
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD