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

90 lines
6.3 KiB
Markdown

# Save and conflict contract
Implemented contract; final qualification is recorded separately in the dated acceptance addendum.
## Canonical Career Profile
`GET /api/career/profile` returns `{ profile, completeness, cvText, revision }`.
The profile comes from canonical relational children/LongTail with the existing legacy blob
fallback. A short read transaction keeps source, revision and raw CV text coherent. Reads do not
backfill/write unversioned children. The public owner-specific load remains read-only.
`revision` is `CareerProfile.Version`; 0 means there is no canonical row. It is independent of
the structured document's schema string `profile.version` and extraction metadata profileVersion.
| Writer | Required source precondition | Commit boundary |
| --- | --- | --- |
| PUT `/career/profile` | Body `expectedVersion` from the draft's GET | Canonical row, replace-all children, append-only history, Identity projection and optional raw text |
| POST `/career/profile/versions/{version}/restore` | Body `expectedVersion` for current canonical source, independent of selected historic version | Restored snapshot appended as a new guarded revision and projection |
| POST `/profile-cv/parse` | Body `expectedVersion` and source text | Applied extraction run, canonical aggregate, projection and import pointers |
| POST `/profile-cv/runs/{id}/accept` | Body `expectedVersion` from the actual reviewed diff | Reviewed merge, canonical aggregate, applied-run metadata and projection |
Missing or negative Career preconditions return 400. A valid stale revision returns 409 with
`code: revision_conflict`. Competing first saves use the existing unique owner constraint;
specific owner-duplicate errors become conflicts. There is no blind-save service overload or
automatic stale retry. Existing-row writes also compare the original version in EF's database
UPDATE predicate, protecting the interval after controller validation.
Controller transactions include Identity updates. A failed Identity result throws before commit;
canonical rows/children/history and import pointers roll back together. Failed tracked changes
are cleared, so a later SaveChanges cannot publish a rejected operation. Successful responses
are constructed within this boundary and returned only after commit. Post-commit parse-artifact
pruning is logged best-effort maintenance and cannot turn the committed save into an error.
Null/omitted `cvText` preserves imported raw text; whitespace explicitly clears it. Identity fields
remain owned by the identity endpoint. The obsolete `/auth/profile` CV projection fields now fail
closed with 400 and guidance to use `/career/profile`; identity-only updates remain supported.
## Job snapshots and related text
Job reads expose integer `version`. Workspace overview and cover-letter reads expose `revision`
for that same job row. Tailored CV draft GET/generation also returns its job revision.
| Writer | Required body field | Success |
| --- | --- | --- |
| PUT `/jobapplications/{id}` | `expectedVersion` from the full editor snapshot | Existing 204 status, `X-Job-Version` committed revision |
| PUT `/jobapplications/{id}/application-drafts` | `expectedVersion` with the answer/recruiter/notes draft source | 204 and `X-Job-Version` |
| PUT `/jobapplications/{id}/cover-letter` | `expectedVersion` with current text/history source | Cover-letter DTO including new revision |
| POST `/jobapplications/{id}/cover-letter/versions/{version}/restore` | `expectedVersion` for current job, independent of history version | Cover-letter DTO including new revision |
| PUT `/jobapplications/{id}/tailored-cv[-draft]` | `expectedVersion` with the legacy text/structured draft source | 204 and `X-Job-Version` |
Missing/invalid/stale job preconditions fail with 409. Every tracked JobApplication mutation,
including partial lifecycle and background writers, advances the application-managed version.
EF's original-version predicate guards the database write; full replacement/no-op saves force
that guard even if the submitted fields compare equal. An overlapping winner cannot be silently
replaced after a slow optional AI summary. Related opportunity fields and timeline events remain
inside the same SaveChanges transaction. No history/events are appended for a rejected write.
Generic job PUT still preserves the separately embedded application answer when editing ordinary
notes. It validates company ownership under tenant filters. Partial application-draft null fields
retain their prior semantics. Source guards do not alter CV-specific null-as-inherit overrides or
submitted/frozen documents.
## Client acknowledgement and recovery
Each draft retains the revision belonging to its actual source. A failed/409 save leaves user text
and choices available. The client never reads the latest revision just before PUT to authorize
an older body, never silently merges/retries a conflict, and never labels a rejected request saved.
The editor excludes overlapping mutations and late responses from older modal/read sessions.
Recovery requires deliberate reload/review. A network failure may leave commit outcome unknown;
the original revision cannot commit twice. Refresh and compare before resubmitting a retained
draft. This repair does not claim recovery of already overwritten historical edits.
## Compatibility and qualification
Older unversioned replacement clients intentionally fail closed. Active application callers and
test fixtures are updated; the extension/bookmarklet create flow does not perform these replacement
writes. An external client must read a source snapshot and carry its revision before replacing it.
No missing-revision compatibility fallback is accepted.
Migration `20261023090000_AddJobRevision` adds the job integer column with default 1. SQLite tests
upgrade populated synthetic storage and roll back/reapply while preserving job content. Career's
existing version column becomes an EF concurrency token without a new physical column. A schema
rollback does not preserve revision identity and is not a qualified production rollback procedure.
SQLite runtime tests and provider SQL generation do not prove MariaDB runtime behavior. The local
Docker Linux engine was unavailable. Mixed old/new server or client deployment, actual MariaDB,
production storage durability and release/rollback remain separate qualifications. No deployment
or production migration was performed.