Files
jobtrackingapp/docs/remaster/DATA_MODEL_REVIEW.md
T
cesnimda f0f178d77e docs(remaster): full-system audit + rebuild-vs-refactor decision
Deep, code-grounded audit of Job Tracker producing the mission deliverables
under docs/remaster/: system audit, bug report, architecture/data-model/AI/UX
reviews, remaster proposal, migration plan, competitor research, and the gated
REBUILD_DECISION.

Verdict: Incremental Refactor (no full rebuild). Evidence: no Critical defects;
hardened cookie/CSRF auth (token never in JS storage), real SSRF defence,
enforced multi-tenancy via global query filters, decoupled provider-swappable
AI service, 135 backend tests. Debt is localised (god controllers/entity,
missing hot-path indexes, prompt-injection hardening, CRA build debt) and
reachable by in-place, test-guarded refactors.

Also harden .gitignore: exclude agent tooling (.claude/, .bg-shell/, .agent.md)
and restore/broaden the runtime-secrets block (**/keys/, **/backups/, exports,
CV artifacts) so nested DataProtection keys can't be committed accidentally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 10:04:03 +02:00

69 lines
4.3 KiB
Markdown

# Data Model Review — Job Tracker
**Companion to:** [SYSTEM_AUDIT_REPORT.md](SYSTEM_AUDIT_REPORT.md)
## 1. Entities (verified in `Models/`)
`JobApplication`, `Company`, `Correspondence`, `Attachment`, `JobEvent`, `TailoredCvDraft`(+`Json`),
`StructuredCvProfile`(+`Json`), `CvExtraction`, `GmailConnection`, `GmailReviewDecision`, `RuleSettings`,
`UserRuleSettings`, `HumanLanguageCatalog`, `SystemEmailSettings`, `ApplicationUser`.
## 2. `JobApplication` — the god entity `[Design flaw]`
~40 columns spanning **eight** distinct concerns on one row:
1. Identity/ownership (`Id`, `OwnerUserId`)
2. Core role (`JobTitle`, `CompanyId`, `Status`, `DateApplied`, `Location`)
3. Salary — **both** free-text (`Salary`) *and* structured (`SalaryMin/Max/Currency/Period`)
4. Workflow (`NextAction`, `FollowUpAt`, `FeedbackRequestedAt`, `RecruiterMessageDraft`)
5. **Denormalised attachment flags** (`HasResume`, `HasCoverLetter`, `HasPortfolio`, `HasOtherAttachment`)
6. Soft delete (`IsDeleted`, `DeletedAt`)
7. Imported content (`Description`, `TranslatedDescription`, `DescriptionLanguage`, `Tags`, `Deadline`, `ShortSummary`)
8. Tailored CV — **both** inline (`TailoredCvText`, `TailoredCvUpdatedAt`) *and* related (`TailoredCvDraft`)
### 2.1 Denormalisation hazard `[Bug risk — High]`
`HasResume/HasCoverLetter/HasPortfolio/HasOtherAttachment` duplicate information already derivable from the
`Attachments` collection. Any code path that adds/removes an attachment without updating the boolean (or
vice-versa) produces a **silent inconsistency** that the attachment-checklist UI will display wrong. These
booleans should be **computed projections**, not stored state. If kept for query performance, they must be
maintained in one place (a domain method) — verify no controller mutates them independently.
### 2.2 Dual tailored-CV source of truth `[Design flaw — High]`
`TailoredCvText` (string on `JobApplication`) vs `TailoredCvDraft`/`TailoredCvDraftJson` (related entities).
Two writable representations of "the tailored CV for this job" with no documented precedence. This is a
classic bug incubator: read one, write the other, and the workspace shows stale content.
### 2.3 CV "versioning" is not modelled `[Design flaw — Medium]`
Product step 8 promises *"CV version is linked to job."* The schema stores a **single current** tailored
text per job, not a **version history**. There is no `CvVersion` table with immutable snapshots. The
promised capability is only partially real. If versioning matters (it should, for A/B and audit), model it
explicitly: `CvVersion(id, ownerUserId, sourceProfileId, jobApplicationId?, content, createdAt, label)`.
## 3. Relationships
- `JobApplication *→1 Company` (FK `CompanyId`) — fine.
- `JobApplication 1→* Correspondence / Attachment / JobEvent` — fine, but **FK columns are unindexed**
(`Correspondence.JobApplicationId`, `JobEvent.JobApplicationId`) → N+1 and slow joins.
- `Correspondence.ExternalThreadId` powers Gmail continuity (D007/D008) — good, but unindexed.
## 4. Indexing `[Performance — High]`
Only `OwnerUserId` is indexed. Every list/board/reminders/analytics query filters on `IsDeleted`
(unindexed), reminders/background jobs filter on `FollowUpAt` (unindexed), and detail loads join on the
unindexed child FKs. **Add:** `IsDeleted`, `(IsDeleted, Status)`, `FollowUpAt`,
`Correspondence.JobApplicationId`, `JobEvent.JobApplicationId`. SQLite- and MySQL-safe.
## 5. Tags/JSON-as-string `[Design flaw — Low]`
`Tags` is a JSON-array string; `TailoredCvDraftJson`/`StructuredCvProfileJson` are JSON blobs. Workable
with EF value converters, but unqueryable. Acceptable given SQLite; revisit if moving fully to MySQL/Postgres
(use native JSON columns).
## 6. Recommended target schema (incremental)
1. Split `JobApplication` into `JobApplication` (core+workflow) + `JobImportContent` (description/translation/
summary/tags) — a 1:1 owned entity — so wide read paths don't drag import blobs.
2. Make attachment booleans computed (drop stored columns after a migration + backfill check).
3. Pick **one** tailored-CV representation (`TailoredCvDraft`) and deprecate `TailoredCvText`.
4. Introduce `CvVersion` for real versioning.
5. Add the five hot-path indexes (do this first — highest value, lowest risk).
All five are additive/behaviour-preserving migrations guarded by the existing test suite.