refactor(db): migrate CV variant history

Move saved CVs and append-only revisions into an additive provider-aware migration. Preserve public slugs, builder settings, job-link nulling, and revision cascades.
This commit is contained in:
cesnimda
2026-08-30 17:28:35 +02:00
parent 778b365f9e
commit bc28893b6f
9 changed files with 206 additions and 92 deletions
@@ -113,6 +113,8 @@ public sealed class MigrationChainTests
Assert.Contains("CREATE TABLE IF NOT EXISTS `TailoredCvDrafts`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `InterviewPrepNotes`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `AiWorkspaceNotes`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariants`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariantVersions`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -579,6 +581,81 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Cv_variant_adoption_preserves_settings_versions_and_relationships()
{
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
await using var db = Context(connection);
var migrator = db.GetService<IMigrator>();
await migrator.MigrateAsync("20260830127000_AdoptJobWorkspaceNotesSchema");
await ExecuteAsync(connection, """
INSERT INTO Companies (Name) VALUES ('Fixture company');
INSERT INTO JobApplications
(CompanyId, JobTitle, DateApplied, SavedAt, Status, ResponseReceived, OwnerUserId,
HasResume, HasCoverLetter, HasPortfolio, HasOtherAttachment, IsDeleted)
VALUES
(1, 'Fixture role', '2026-08-01T09:00:00', '2026-07-20T09:00:00',
'Applied', 0, 'owner-fixture', 0, 0, 0, 0, 0);
CREATE TABLE "CvVariants" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariants" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "PublicSlug" TEXT NOT NULL, "Name" TEXT NOT NULL,
"JobApplicationId" INTEGER NULL, "SettingsJson" TEXT NOT NULL,
"IsPublic" INTEGER NOT NULL, "Version" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL, "UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariants_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE SET NULL
);
CREATE TABLE "CvVariantVersions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariantVersions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "CvVariantId" INTEGER NOT NULL,
"Version" INTEGER NOT NULL, "SettingsJson" TEXT NOT NULL,
"Source" TEXT NOT NULL, "CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariantVersions_CvVariants_CvVariantId"
FOREIGN KEY ("CvVariantId") REFERENCES "CvVariants" ("Id") ON DELETE CASCADE
);
INSERT INTO "CvVariants"
("OwnerUserId", "PublicSlug", "Name", "JobApplicationId", "SettingsJson",
"IsPublic", "Version", "CreatedAtUtc", "UpdatedAtUtc")
VALUES ('owner-fixture', 'public-fixture', 'Backend CV', 1,
'{"templateId":"code","accent":"#123456"}', 1, 2,
'2026-08-30T09:00:00+00:00', '2026-08-30T09:10:00+00:00');
INSERT INTO "CvVariantVersions"
("OwnerUserId", "CvVariantId", "Version", "SettingsJson", "Source", "CreatedAtUtc")
VALUES ('owner-fixture', 1, 1, '{"templateId":"code"}', 'create',
'2026-08-30T09:00:00+00:00'),
('owner-fixture', 1, 2, '{"templateId":"code","accent":"#123456"}', 'autosave',
'2026-08-30T09:10:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("public-fixture", await ScalarAsync<string>(connection,
"SELECT PublicSlug FROM CvVariants WHERE Id = 1;"));
Assert.Equal(2L, await ScalarAsync<long>(connection,
"SELECT COUNT(*) FROM CvVariantVersions WHERE CvVariantId = 1;"));
await migrator.MigrateAsync("20260830127000_AdoptJobWorkspaceNotesSchema");
Assert.Equal("Backend CV", await ScalarAsync<string>(connection,
"SELECT Name FROM CvVariants WHERE Id = 1;"));
await migrator.MigrateAsync();
Assert.Equal(4L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name IN (
'IX_CvVariants_JobApplicationId',
'IX_CvVariants_OwnerUserId_UpdatedAtUtc',
'IX_CvVariants_PublicSlug',
'IX_CvVariantVersions_CvVariantId_Version');
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(1L, await ScalarAsync<long>(connection,
"SELECT COUNT(*) FROM CvVariants WHERE JobApplicationId IS NULL;"));
await ExecuteAsync(connection, "DELETE FROM CvVariants WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM CvVariantVersions;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();
@@ -0,0 +1,105 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves CV variants and append-only version history into migration ownership while preserving
/// existing builder settings, public slugs, and job-tailoring relationships.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830128000_AdoptCvVariantSchema")]
public sealed class AdoptCvVariantSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `CvVariants` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`PublicSlug` varchar(64) NOT NULL,
`Name` varchar(255) NOT NULL,
`JobApplicationId` int NULL,
`SettingsJson` longtext NOT NULL,
`IsPublic` tinyint(1) NOT NULL,
`Version` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvVariants_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE SET NULL
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_CvVariants_JobApplicationId`
ON `CvVariants` (`JobApplicationId`);
CREATE INDEX IF NOT EXISTS `IX_CvVariants_OwnerUserId_UpdatedAtUtc`
ON `CvVariants` (`OwnerUserId`, `UpdatedAtUtc`);
CREATE UNIQUE INDEX IF NOT EXISTS `IX_CvVariants_PublicSlug`
ON `CvVariants` (`PublicSlug`);
CREATE TABLE IF NOT EXISTS `CvVariantVersions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`CvVariantId` int NOT NULL,
`Version` int NOT NULL,
`SettingsJson` longtext NOT NULL,
`Source` varchar(100) NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvVariantVersions_CvVariants_CvVariantId`
FOREIGN KEY (`CvVariantId`) REFERENCES `CvVariants` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_CvVariantVersions_CvVariantId_Version`
ON `CvVariantVersions` (`CvVariantId`, `Version`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "CvVariants" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariants" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"PublicSlug" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"JobApplicationId" INTEGER NULL,
"SettingsJson" TEXT NOT NULL,
"IsPublic" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariants_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS "IX_CvVariants_JobApplicationId"
ON "CvVariants" ("JobApplicationId");
CREATE INDEX IF NOT EXISTS "IX_CvVariants_OwnerUserId_UpdatedAtUtc"
ON "CvVariants" ("OwnerUserId", "UpdatedAtUtc");
CREATE UNIQUE INDEX IF NOT EXISTS "IX_CvVariants_PublicSlug"
ON "CvVariants" ("PublicSlug");
CREATE TABLE IF NOT EXISTS "CvVariantVersions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariantVersions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"CvVariantId" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"SettingsJson" TEXT NOT NULL,
"Source" TEXT NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariantVersions_CvVariants_CvVariantId"
FOREIGN KEY ("CvVariantId") REFERENCES "CvVariants" ("Id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "IX_CvVariantVersions_CvVariantId_Version"
ON "CvVariantVersions" ("CvVariantId", "Version");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve CV documents and revision history created before migration ownership.
}
}
@@ -705,44 +705,6 @@ public static class StartupInitializationExtensions
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CareerLanguages_OwnerUserId_CareerProfileId_SortOrder" ON "CareerLanguages" ("OwnerUserId", "CareerProfileId", "SortOrder");""");
}
// Phase 4 CV Builder: a variant is a lens over the master profile; versions are its
// autosave history. Reconciler-owned (not migration-owned) so the schema is MySQL-safe
// on prod -- see the AddCvVariants migration note. docs/architecture/cv-builder.md.
static void EnsureCvBuilderTables(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "CvVariants" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariants" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"PublicSlug" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"JobApplicationId" INTEGER NULL,
"SettingsJson" TEXT NOT NULL,
"IsPublic" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariants_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE SET NULL
);
""");
Exec(c, """
CREATE TABLE IF NOT EXISTS "CvVariantVersions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_CvVariantVersions" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"CvVariantId" INTEGER NOT NULL,
"Version" INTEGER NOT NULL,
"SettingsJson" TEXT NOT NULL,
"Source" TEXT NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_CvVariantVersions_CvVariants_CvVariantId" FOREIGN KEY ("CvVariantId") REFERENCES "CvVariants" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvVariants_JobApplicationId" ON "CvVariants" ("JobApplicationId");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvVariants_OwnerUserId_UpdatedAtUtc" ON "CvVariants" ("OwnerUserId", "UpdatedAtUtc");""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_CvVariants_PublicSlug" ON "CvVariants" ("PublicSlug");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_CvVariantVersions_CvVariantId_Version" ON "CvVariantVersions" ("CvVariantId", "Version");""");
}
// Phase 5 AI Workspace: append-only AI interaction history per job application.
static void EnsureAiInteractionsTable(DbConnection c)
{
@@ -841,7 +803,6 @@ public static class StartupInitializationExtensions
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
EnsureCvBuilderTables(conn);
EnsureAiInteractionsTable(conn);
EnsureApplicationChecklistTable(conn);
EnsureCoverLetterVersionsTable(conn);
@@ -1227,12 +1188,10 @@ public static class StartupInitializationExtensions
cmd.ExecuteNonQuery();
}
// Phase 4 CV Builder + Phase 5 AI Workspace. Reconciler-owned rather than
// migration-owned: those migrations were scaffolded against SQLite, so on MariaDB
// they emit TEXT datetimes and a PK with no AUTO_INCREMENT, and the composite
// index over a TEXT column then exceeds MySQL's 3072-byte key limit -- which
// crashed backend startup on prod. Drop any such half-built table (only when it
// holds no rows) and rebuild from the correct MySQL DDL below.
// Historical Phase 4/5 migrations were scaffolded against SQLite, so on MariaDB
// they could emit TEXT datetimes and a PK with no AUTO_INCREMENT, then fail while
// indexing TEXT columns. Drop only malformed empty tables before the current
// provider-aware adoption migrations run; populated tables are never replaced.
// Children first: CvVariantVersions FKs into CvVariants.
DropMalformedMySqlTable(conn, "CvVariantVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "CvVariants", "UpdatedAtUtc", "datetime");
@@ -1241,43 +1200,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "CvVariants") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CvVariants` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`PublicSlug` varchar(64) NOT NULL,
`Name` varchar(255) NOT NULL,
`JobApplicationId` int NULL,
`SettingsJson` longtext NOT NULL,
`IsPublic` tinyint(1) NOT NULL,
`Version` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvVariants_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE SET NULL
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "CvVariantVersions") && HasMySqlTable(conn, "CvVariants"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `CvVariantVersions` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`CvVariantId` int NOT NULL,
`Version` int NOT NULL,
`SettingsJson` longtext NOT NULL,
`Source` varchar(100) NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_CvVariantVersions_CvVariants_CvVariantId` FOREIGN KEY (`CvVariantId`) REFERENCES `CvVariants` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "AiInteractions") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
@@ -18,6 +18,8 @@ internal static class StartupSchemaOwnership
"Correspondences",
"CvExtractionRuns",
"CvUploadArtifacts",
"CvVariants",
"CvVariantVersions",
"EmailDrafts",
"EmailSendAttempts",
"GmailConnections",
@@ -59,8 +61,6 @@ internal static class StartupSchemaOwnership
"CareerProjects",
"CareerSkills",
"CoverLetterVersions",
"CvVariants",
"CvVariantVersions",
"InterviewPrepItems",
};
+1 -1
View File
@@ -558,7 +558,7 @@ SEC-008 implements the same durable state machine with `<final>.uploading` and `
### P3-1 — Reduce dual schema ownership incrementally
**Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Fourteen formerly reconciler-created tables—including authentication support, email-provider connections, CV extraction persistence, tailored CV drafts, and persisted job-workspace AI notes—have moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite runtime proof and generated MariaDB SQL. The Gmail decision migration also closes its missing MariaDB creation path. Twenty-one startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-201.
**Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Sixteen formerly reconciler-created tables—including authentication support, email-provider connections, CV extraction persistence, tailored CV drafts, job-workspace AI notes, and CV variant history—have moved through additive provider-aware migrations with legacy-row preservation, downgrade/retry safety, startup-DDL removal, fresh SQLite runtime proof and generated MariaDB SQL. The Gmail decision migration also closes its missing MariaDB creation path. Nineteen startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-202.
- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps.
- **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
+1
View File
@@ -233,3 +233,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
| V-199 | Ownership/migration chain; populated adoption/downgrade/retry; FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move CV upload artifacts and extraction runs to migration ownership without losing import history | PASS — focused ownership/migration 11/11; artifact hash and structured result survive adoption, downgrade and re-upgrade; all three indexes exist; artifact deletion sets the run link null; generated MariaDB SQL contains provider-safe parent/child DDL; startup creates neither table; full backend 727/727 | MariaDB SQL generated only; no provider account or production migration. Guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-four reconciler-owned tables remain | JT-019 CV extraction persistence transfer complete; tailored CV and career groups continue incrementally |
| V-200 | Ownership/migration chain; populated adoption/downgrade/retry; FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move job-specific tailored CV drafts to migration ownership without losing generated or edited content | PASS — focused ownership/migration 12/12; summary, custom-section and context data survive adoption, downgrade and re-upgrade; both indexes exist; deleting the parent application cascades to the draft; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 728/728 | MariaDB SQL generated only; no provider account or production migration. Guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-three reconciler-owned tables remain | JT-019 tailored CV transfer complete; career-profile aggregate remains a dependency-aware future batch |
| V-201 | Ownership/migration chain; populated adoption/downgrade/retry; FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move persisted interview-preparation and irregular AI workspace notes to migration ownership without losing reviewed outputs | PASS — focused ownership/migration 13/13; interview summary and candidate-fit JSON survive adoption, downgrade and re-upgrade; both uniqueness indexes exist; deleting the parent application cascades through both tables; generated MariaDB SQL is provider-safe; startup creates neither table; full backend 729/729 | MariaDB SQL generated only; no provider account or production migration. Guarded MariaDB index/auto-increment repairs remain temporarily. Twenty-one reconciler-owned tables remain | JT-019 job-workspace note transfer complete; CV variants and career-profile aggregates remain dependency-aware batches |
| V-202 | Ownership/migration chain; populated adoption/downgrade/retry; relationship/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move saved CV variants and append-only revision history to migration ownership without losing user documents | PASS — focused ownership/migration 14/14; public slug, settings and two revisions survive adoption, downgrade and re-upgrade; four indexes exist; deleting the job nulls only the CV link and deleting the CV cascades through revisions; generated MariaDB SQL is provider-safe; startup creates neither table; full backend 730/730 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Nineteen reconciler-owned tables remain | JT-019 CV variant aggregate transfer complete; career-profile aggregate and remaining feature tables continue incrementally |
+8 -4
View File
@@ -67,7 +67,8 @@ already-correct database. Two consequences worth knowing:
Created by EF migrations, never by the reconciler:
`AccountDeletionFiles`, `AccountDeletionRequests`, `AiUsageRecords`, `AiWorkspaceNotes`, `Attachments`, `Companies`,
`Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `EmailDrafts`, `EmailSendAttempts`, `GmailConnections`,
`Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `CvVariants`, `CvVariantVersions`,
`EmailDrafts`, `EmailSendAttempts`, `GmailConnections`,
`GmailReviewDecisions`, `ImapConnections`, `InterviewPrepNotes`, `JobApplications`, `JobEvents`, `Jobs`,
`MicrosoftGraphConnections`, `RuleSettings`, `SystemEmailSettings`, `TailoredCvDrafts`,
`TrustedDevices`, `TwoFactorRecoveryCodes`, `UserNotifications`, `UserOperations`, `UserRuleSettings`,
@@ -105,6 +106,10 @@ The persisted job-workspace note group (`InterviewPrepNotes` and `AiWorkspaceNot
`20260830127000_AdoptJobWorkspaceNotesSchema`. Both caches retain their unique owner/job keys and
remain application-owned through `ON DELETE CASCADE`.
The CV builder aggregate (`CvVariants` and `CvVariantVersions`) moved in
`20260830128000_AdoptCvVariantSchema`. Public slugs remain unique, deleting a linked application
sets the CV link to null, and deleting a CV cascades through its append-only revision history.
The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT`
primary key) and may seed the default `RuleSettings` row — but it must never `CREATE TABLE` them.
It used to create `RuleSettings`, which is precisely why a clean install failed with
@@ -116,8 +121,7 @@ Created by `StartupInitializationExtensions`, with a **no-op migration** holding
`CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`,
`CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`),
`CvVariants`, `CvVariantVersions`, `AiInteractions`,
`ApplicationChecklistItems`, `CoverLetterVersions`, and `InterviewPrepItems`.
`AiInteractions`, `ApplicationChecklistItems`, `CoverLetterVersions`, and `InterviewPrepItems`.
The seven ASP.NET Identity tables are also currently reconciler-owned, despite older wording that
called them migration-owned: `AspNetRoles`, `AspNetUsers`, `AspNetRoleClaims`, `AspNetUserClaims`,
@@ -132,7 +136,7 @@ No-op migrations, each with a comment explaining why:
| Migration | Tables |
|---|---|
| `20260717222917_AddCareerProfileRelationalChildren` | the six CareerProfile children |
| `20260718074509_AddCvVariants` | `CvVariants`, `CvVariantVersions` |
| `20260718074509_AddCvVariants` | historical no-op; ownership transferred by `20260830128000_AdoptCvVariantSchema` |
| `20260718131138_AddAiInteractions` | `AiInteractions` |
| `20260719085904_AddApplicationChecklistItems` | `ApplicationChecklistItems` |
| `20260719094728_SyncCareerChildKeyLengths` | snapshot sync only |
+6 -2
View File
@@ -31,6 +31,8 @@ cohesive table groups. It does not attempt to delete the legacy reconciler whole
generated and manually edited content plus its `ON DELETE CASCADE` application relationship.
- Added `20260830127000_AdoptJobWorkspaceNotesSchema` for interview preparation and irregular AI
workspace results, retaining reviewed output and both application cascade relationships.
- Added `20260830128000_AdoptCvVariantSchema` for saved CVs and their append-only revision history,
retaining public slugs, builder settings, and job-link/version retention semantics.
- Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy
compatibility code to retire one dependency group at a time.
@@ -60,14 +62,16 @@ migration.
downgrade, and re-upgrade; deleting the parent application still deletes its draft.
- Representative interview-preparation and candidate-fit outputs survive adoption, downgrade, and
re-upgrade; deleting the parent application still deletes both cached note types.
- A representative CV and two saved revisions survive adoption, downgrade, and re-upgrade; deleting
its job clears only the optional link, while deleting the CV cascades through its revisions.
- Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL.
- Full backend: 729/729 passed after the job-workspace note transfer.
- Full backend: 730/730 passed after the CV variant and history transfer.
- Fresh application startup over a new disposable SQLite database applied
`20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state.
## Remaining JT-019 work
Twenty-one model tables remain startup-created, including the Identity group and several tables
Nineteen model tables remain startup-created, including the Identity group and several tables
with parent dependencies. Transfer them in small dependency-aware migrations with blank, populated,
retry and MariaDB runtime proof. Column/index repairs must remain until historical upgrade fixtures
prove each one redundant.
+2 -1
View File
@@ -51,6 +51,7 @@ Updated: 2026-08-30
- Moved CV upload artifacts and extraction runs into a provider-aware migration; existing hashes, structured results, indexes, and nullable artifact retention semantics are preserved.
- Moved job-specific tailored CV drafts into a provider-aware migration; generated and edited content, indexes, and application cascade semantics are preserved.
- Moved persisted interview-preparation and AI workspace notes into one provider-aware migration; reviewed outputs, uniqueness, and application cascades are preserved.
- Moved saved CV variants and append-only revision history into one provider-aware migration; public slugs, settings, indexes, job-link nulling, and history cascades are preserved.
### In progress
@@ -81,7 +82,7 @@ Updated: 2026-08-30
- Focused frontend: 2 suites, 6 tests passed.
- Full frontend: 64 suites, 272 tests passed.
- Next production build and TypeScript: passed.
- Full backend: 729/729 tests passed after the job-workspace notes JT-019 transfer.
- Full backend: 730/730 tests passed after the CV variant/history JT-019 transfer.
- Portable Playwright launcher: resolved the user-local .NET 9 SDK; backend Release build passed with 0 warnings/errors.
- Playwright: initial full run 9/10 exposed the intentional mobile Settings control change; updated focused rerun passed 1/1. A final complete browser rerun remains in the end-of-batch gate.
- Focused backend match/intelligence verification: 34/34 passed.