68 lines
4.3 KiB
Markdown
68 lines
4.3 KiB
Markdown
# 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.
|