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

6.3 KiB

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 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.