Files
jobtrackingapp/docs/redesign/phase-9/rd21-remediation/ROOT_CAUSE.md
T

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.