90 lines
5.9 KiB
Markdown
90 lines
5.9 KiB
Markdown
# RD21 investigation (23 October 2026)
|
|
|
|
Status: SQLite repair implemented and locally qualified. Formal supported-provider closure remains
|
|
UNRESOLVED; see the [dated acceptance addendum](ACCEPTANCE_ADDENDUM_2026-10-23.md).
|
|
|
|
## 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 preserves the revision belonging to each draft, blocks conflicting
|
|
in-flight actions, rejects old responses, and displays 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. Additional `default` and
|
|
`desktop-windows` context info probes also failed with their named pipes absent. Command lookup
|
|
found no mariadbd/mysqld/mariadb/mysql executable, and neither Program Files installation root
|
|
contained a MariaDB/MySQL directory. No daemon installation/restart was attempted. MariaDB runtime
|
|
qualification is outstanding; SQLite and provider script generation
|
|
cannot substitute for a MariaDB persistence/concurrency run.
|
|
|
|
## Integration review findings during repair
|
|
|
|
The first repair passed its backend and component suites, but whole-branch connected review found
|
|
two integration gaps before final qualification. Neither is hidden by those passing totals:
|
|
|
|
- The application-drafts client consumes `X-Job-Version` after 204, while the existing cross-origin
|
|
CORS policy does not expose that response header. APIRequest tests can read it, but browser Axios
|
|
cannot. The database can therefore commit while the browser reports a missing committed revision.
|
|
Actual cross-origin browser proof is required in addition to raw HTTP assertions.
|
|
- Cover-letter saves advance the shared job version. Answers initially consumes a cached overview
|
|
snapshot; switching from a successful letter save to Answers can therefore submit an older source
|
|
revision and receive an avoidable 409. A new Answers editor needs a coherent fresh source before
|
|
editing. Updating only its revision would incorrectly authorize stale body fields; dirty drafts
|
|
must retain their original source through later parent refreshes.
|
|
|
|
The broad browser run was stopped after 22 passing cases while these findings were addressed. It
|
|
is an interrupted attempt, not a full-suite pass. Final results must qualify the corrected source.
|
|
|
|
## 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.
|