docs(rd21): record reproduced data loss and remediation scope
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# RD21 investigation (23 October 2026)
|
||||
|
||||
Status: repair and qualification in progress; this is not acceptance.
|
||||
|
||||
## Original failure
|
||||
|
||||
The unchanged Phase 9 characterization was executed on the exact baseline
|
||||
`8e7e3f6387544e24082ebe2c107c0b868897959e`. Both independently authenticated
|
||||
editors read the same synthetic record. Editor A changes and successfully saves one field.
|
||||
Editor B changes a different field in its older complete snapshot. B's request is accepted and
|
||||
replaces A's field with its original value. Subsequent authenticated reads show the loss.
|
||||
See [original-reproduction.json](original-reproduction.json). Career stale response: 200;
|
||||
job stale response: 204. Both `lostFirstEdit` values are true. The characterization's one passing
|
||||
test establishes that the unsafe behavior was reproduced; it is not a safety pass.
|
||||
|
||||
New native SQLite tests additionally query persisted rows through independent DbContexts
|
||||
immediately after acknowledgement. The original job safety assertion failed because the stale
|
||||
request returned `NoContentResult` instead of a conflict. Career safety tests failed because the
|
||||
stale request returned success and because an injected Identity failure left canonical rows
|
||||
committed despite the failure response. These are separate deterministic regression assertions.
|
||||
|
||||
## Mechanisms
|
||||
|
||||
1. Generic job PUT reads the current row, maps the client's complete replacement over it and
|
||||
calls SaveChanges. The request has no source revision; the entity has no concurrency token.
|
||||
A fresh request context therefore accepts an arbitrarily old snapshot. This is persisted data
|
||||
replacement, independently of client display state. The optional awaited summary generation
|
||||
also creates an interval between read and write; checking only at controller entry is unsafe.
|
||||
2. Career PUT and restore similarly lack an editor revision. The canonical service increments
|
||||
Version but does not compare the editor's version or use it as an EF concurrency token.
|
||||
Children and history are transactionally saved, then the transaction commits. The controller
|
||||
updates ApplicationUser.ProfileCvStructureJson/CvText afterward. Identity failure can leave
|
||||
canonical and legacy projections inconsistent despite a failed operation response.
|
||||
3. Career parse and reviewed import acceptance also call the canonical replacement service and
|
||||
update the projection/application metadata separately. They must participate in the same guard
|
||||
and transaction; fixing only manual PUT would leave alternate replacement paths open.
|
||||
|
||||
## Directly related text writers
|
||||
|
||||
SQLite tests reproduce the same persisted replacement through cover-letter save and application
|
||||
drafts: a first acknowledged `First approved text` becomes `Stale snapshot text` after a later
|
||||
request from the stale editor. These routes replace fields also writable through the generic job
|
||||
snapshot. They are included in the bounded repair. Cover-letter history alone does not reject
|
||||
stale approval or keep the current approved text intact. Legacy tailored-CV text replacement is
|
||||
also an application-owned snapshot writer and must fail closed without its source revision.
|
||||
|
||||
## Client lifecycle
|
||||
|
||||
Career has an existing synchronous mutation guard and disables its editor while saving. Its error
|
||||
paths retain drafts, but it cannot detect another session's change without server revision data.
|
||||
The shared job dialog does not guard older GET responses, and remains editable/closable during
|
||||
PUT; a successful submitted snapshot can close a dialog containing later unsent edits. The
|
||||
frontend correction will preserve the revision belonging to each draft, block conflicting
|
||||
in-flight actions, reject old responses, and display recoverable conflicts without blind retry.
|
||||
|
||||
## Provider limitation
|
||||
|
||||
The read-only `rtk proxy docker info --format '{{json .ServerVersion}}'` probe on 23 October
|
||||
failed: `dockerDesktopLinuxEngine` named pipe does not exist. No daemon installation/restart was
|
||||
attempted. MariaDB runtime qualification is outstanding; SQLite and provider script generation
|
||||
cannot substitute for a MariaDB persistence/concurrency run.
|
||||
|
||||
## Historical recovery
|
||||
|
||||
No evidence establishes recovery of edits already overwritten before this repair. Canonical
|
||||
history may contain a prior profile snapshot, but availability and user intent must be examined
|
||||
per record. No production database has been accessed and no historical recovery is claimed.
|
||||
Reference in New Issue
Block a user