4d7d6456e3
Fix round 1 (review task-9-review.md), all rulings: 1. (Important 1) ApplyReplicatedAsync is no longer trusted blindly. Every apply is confirmed by a store.GetAsync read-back before ledgering: a tombstoned local row (routine remove-then-re-add) is safely re-stamped past the tombstone and re-applied (confirmed neither UpdatedUtc nor Revision is AEAD-bound in AesGcmEnvelopeCipher, so the bump cannot break decryption); a live local row with different ciphertext (a strictly-newer out-of-band secret) is left untouched, never ledgered, never verified against, and reported with a distinct Failed message so a later central tombstone can never delete it via this mirror. 2. (Important 2) Per-row try/catch (rethrow only on ct.IsCancellationRequested) so one poisoned row (e.g. an unrecognised ContentType from a newer central) no longer blocks every row after it. 3. (Important 3) Optional ISecretCacheInvalidator, invalidated after every successful apply/delete and before verify, so a rotated/removed secret's plaintext cannot be served stale for the remainder of the resolver's TTL. 4. (Minor 4) Loop and VerifyAsync OperationCanceledException handling now filters on ct/stoppingToken.IsCancellationRequested specifically, rather than swallowing every OCE. 5. (Minor 5) Class remarks note the mirror assumes a node-local ISecretStore; Task 11 will reject SqlServer secrets replication on the Site role. 6. (Minor 6/7) Added cancellation + concurrent-callers tests, dropped the redundant FailFor (tests now use ThrowFor/ReturnNullFor to exercise distinct decrypt-failure paths), and Dispose() now disposes the gate SemaphoreSlim. Verify: dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests --filter FullyQualifiedName~DeployedSecretsMirrorTests (24/24); full SiteRuntime.Tests suite 876/876.