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

4.3 KiB

Data Model Review — Job Tracker

Companion to: 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).

  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.