refactor(db): migrate interview preparation

Move durable user-owned interview preparation into an additive provider-aware migration. Preserve questions, answers, practice state, ordering, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:58:55 +02:00
parent 934b35a1e0
commit a906916acc
10 changed files with 148 additions and 55 deletions
@@ -117,6 +117,7 @@ public sealed class MigrationChainTests
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariantVersions`", script, StringComparison.Ordinal); Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariantVersions`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems`", script, StringComparison.Ordinal); Assert.Contains("CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CoverLetterVersions`", script, StringComparison.Ordinal); Assert.Contains("CREATE TABLE IF NOT EXISTS `CoverLetterVersions`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `InterviewPrepItems`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal); Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All( Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value), Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -820,6 +821,63 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync()); Assert.Empty(await db.Database.GetPendingMigrationsAsync());
} }
[Fact]
public async Task Interview_prep_item_adoption_preserves_user_content_and_practice_state()
{
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("20260830131000_AdoptCoverLetterVersionSchema");
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 "InterviewPrepItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "JobApplicationId" INTEGER NOT NULL,
"Category" TEXT NOT NULL, "Title" TEXT NOT NULL, "Content" TEXT NULL,
"Source" TEXT NOT NULL, "IsPrepared" INTEGER NOT NULL, "SortOrder" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL, "UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepItems_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
INSERT INTO "InterviewPrepItems"
("OwnerUserId", "JobApplicationId", "Category", "Title", "Content", "Source",
"IsPrepared", "SortOrder", "CreatedAtUtc", "UpdatedAtUtc")
VALUES
('owner-fixture', 1, 'star', 'Describe a difficult incident',
'Situation, task, action, result', 'user', 1, 10,
'2026-08-30T09:00:00+00:00', '2026-08-30T09:10:00+00:00'),
('owner-fixture', 1, 'technical', 'Explain dependency injection',
'Accepted suggestion, then edited', 'ai', 0, 20,
'2026-08-30T09:00:00+00:00', '2026-08-30T09:15:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("Situation, task, action, result", await ScalarAsync<string>(connection,
"SELECT Content FROM InterviewPrepItems WHERE SortOrder = 10;"));
Assert.Equal(1L, await ScalarAsync<long>(connection,
"SELECT IsPrepared FROM InterviewPrepItems WHERE SortOrder = 10;"));
await migrator.MigrateAsync("20260830131000_AdoptCoverLetterVersionSchema");
Assert.Equal("ai", await ScalarAsync<string>(connection,
"SELECT Source FROM InterviewPrepItems WHERE SortOrder = 20;"));
await migrator.MigrateAsync();
Assert.Equal(1L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name = 'IX_InterviewPrepItems_Owner_Job_Sort';
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM InterviewPrepItems;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection) private static JobTrackerContext Context(SqliteConnection connection)
{ {
var currentUser = new Mock<ICurrentUserService>(); var currentUser = new Mock<ICurrentUserService>();
@@ -0,0 +1,70 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves durable user-owned interview preparation into migration ownership without replacing
/// questions, answers, research, or practice state.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830132000_AdoptInterviewPrepItemSchema")]
public sealed class AdoptInterviewPrepItemSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `InterviewPrepItems` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Category` varchar(32) NOT NULL,
`Title` varchar(500) NOT NULL,
`Content` longtext NULL,
`Source` varchar(16) NOT NULL,
`IsPrepared` tinyint(1) NOT NULL,
`SortOrder` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_InterviewPrepItems_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE INDEX IF NOT EXISTS `IX_InterviewPrepItems_Owner_Job_Sort`
ON `InterviewPrepItems` (`OwnerUserId`, `JobApplicationId`, `SortOrder`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "InterviewPrepItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Category" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Content" TEXT NULL,
"Source" TEXT NOT NULL,
"IsPrepared" INTEGER NOT NULL,
"SortOrder" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepItems_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "IX_InterviewPrepItems_Owner_Job_Sort"
ON "InterviewPrepItems" ("OwnerUserId", "JobApplicationId", "SortOrder");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve durable user-authored preparation from pre-migration installations.
}
}
+1 -2
View File
@@ -8,8 +8,7 @@ namespace JobTrackerApi.Models;
// own questions — and nothing regenerates it. // own questions — and nothing regenerates it.
// //
// One table for every category rather than a table per category: they differ only by label, and a new // One table for every category rather than a table per category: they differ only by label, and a new
// category must not need a migration. Reconciler-owned // category must not need a migration. Migration-owned through an additive provider-aware adoption.
// (docs/infrastructure/database-ownership.md); its migration is a no-op.
public sealed class InterviewPrepItem public sealed class InterviewPrepItem
{ {
public int Id { get; set; } public int Id { get; set; }
@@ -712,32 +712,9 @@ public static class StartupInitializationExtensions
EnsureColumn(c, "AiInteractions", "EstimatedTokenCount", "ALTER TABLE AiInteractions ADD COLUMN EstimatedTokenCount INTEGER NOT NULL DEFAULT 0;"); EnsureColumn(c, "AiInteractions", "EstimatedTokenCount", "ALTER TABLE AiInteractions ADD COLUMN EstimatedTokenCount INTEGER NOT NULL DEFAULT 0;");
} }
// Phase 5.5: user-owned interview preparation (not the AI cache InterviewPrepNote).
static void EnsureInterviewPrepItemsTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "InterviewPrepItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"Category" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Content" TEXT NULL,
"Source" TEXT NOT NULL,
"IsPrepared" INTEGER NOT NULL,
"SortOrder" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepItems_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_InterviewPrepItems_Owner_Job_Sort" ON "InterviewPrepItems" ("OwnerUserId", "JobApplicationId", "SortOrder");""");
}
ReconcileGmailConnectionColumns(conn); ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn); EnsureCareerProfileTables(conn);
ReconcileAiInteractionUsageColumns(conn); ReconcileAiInteractionUsageColumns(conn);
EnsureInterviewPrepItemsTable(conn);
// Once the base app tables exist, provision this reconciler-owned schema set and // Once the base app tables exist, provision this reconciler-owned schema set and
// stamp its historical migration before later migrations rebuild JobApplications. // stamp its historical migration before later migrations rebuild JobApplications.
@@ -1131,27 +1108,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime"); DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime"); DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "InterviewPrepItems") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `InterviewPrepItems` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`Category` varchar(32) NOT NULL,
`Title` varchar(500) NOT NULL,
`Content` longtext NULL,
`Source` varchar(16) NOT NULL,
`IsPrepared` tinyint(1) NOT NULL,
`SortOrder` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_InterviewPrepItems_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
EnsureMySqlAutoIncrementPrimaryKey(conn, "InterviewPrepItems", "Id"); EnsureMySqlAutoIncrementPrimaryKey(conn, "InterviewPrepItems", "Id");
EnsureMySqlIndex(conn, "InterviewPrepItems", "IX_InterviewPrepItems_Owner_Job_Sort", "`OwnerUserId`, `JobApplicationId`, `SortOrder`"); EnsureMySqlIndex(conn, "InterviewPrepItems", "IX_InterviewPrepItems_Owner_Job_Sort", "`OwnerUserId`, `JobApplicationId`, `SortOrder`");
@@ -29,6 +29,7 @@ internal static class StartupSchemaOwnership
"GmailReviewDecisions", "GmailReviewDecisions",
"ImapConnections", "ImapConnections",
"InterviewPrepNotes", "InterviewPrepNotes",
"InterviewPrepItems",
"JobApplications", "JobApplications",
"JobEvents", "JobEvents",
"Jobs", "Jobs",
@@ -61,7 +62,6 @@ internal static class StartupSchemaOwnership
"CareerProfileVersions", "CareerProfileVersions",
"CareerProjects", "CareerProjects",
"CareerSkills", "CareerSkills",
"InterviewPrepItems",
}; };
// Historical migrations contain guarded compatibility bootstraps for these tables so direct // Historical migrations contain guarded compatibility bootstraps for these tables so direct
+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 ### 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. Nineteen formerly reconciler-created tables—including authentication support, email-provider connections, CV persistence/history, job-workspace AI notes, append-only AI interactions, workflow checklists, and cover-letter 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. Sixteen startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-205. **Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Twenty formerly reconciler-created tables—including authentication support, email-provider connections, CV persistence/history, job-workspace AI notes, append-only AI interactions, workflow checklists, cover-letter history, and durable interview preparation—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. Fifteen startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-206.
- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps. - **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. - **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
+1
View File
@@ -237,3 +237,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
| V-203 | Ownership/migration chain; populated adoption/downgrade/retry; usage/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move append-only AI interaction history to explicit migration ownership without losing generated results or usage data | PASS — focused ownership/migration 15/15; result JSON, mode, provider and usage counters survive adoption, downgrade and re-upgrade; both indexes exist; deleting the parent application cascades through history; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 731/731 | MariaDB SQL generated only; no provider account or production migration. Historical compatibility bootstrap and guarded counter/shape/index repairs remain. Eighteen reconciler-owned tables remain | JT-019 AI interaction transfer complete; checklist/document/preparation tables continue incrementally | | V-203 | Ownership/migration chain; populated adoption/downgrade/retry; usage/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move append-only AI interaction history to explicit migration ownership without losing generated results or usage data | PASS — focused ownership/migration 15/15; result JSON, mode, provider and usage counters survive adoption, downgrade and re-upgrade; both indexes exist; deleting the parent application cascades through history; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 731/731 | MariaDB SQL generated only; no provider account or production migration. Historical compatibility bootstrap and guarded counter/shape/index repairs remain. Eighteen reconciler-owned tables remain | JT-019 AI interaction transfer complete; checklist/document/preparation tables continue incrementally |
| V-204 | Ownership/migration chain; populated adoption/downgrade/retry; workflow/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move application workflow checklist state to migration ownership without losing automatic or manual progress | PASS — focused ownership/migration 16/16; auto-completed system and pending manual items survive adoption, downgrade and re-upgrade; idempotency/order indexes exist; deleting the parent application cascades through checklist items; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 732/732 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Seventeen reconciler-owned tables remain | JT-019 checklist transfer complete; cover-letter and interview-preparation tables continue incrementally | | V-204 | Ownership/migration chain; populated adoption/downgrade/retry; workflow/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move application workflow checklist state to migration ownership without losing automatic or manual progress | PASS — focused ownership/migration 16/16; auto-completed system and pending manual items survive adoption, downgrade and re-upgrade; idempotency/order indexes exist; deleting the parent application cascades through checklist items; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 732/732 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Seventeen reconciler-owned tables remain | JT-019 checklist transfer complete; cover-letter and interview-preparation tables continue incrementally |
| V-205 | Ownership/migration chain; populated adoption/downgrade/retry; document/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move append-only cover-letter revision history to migration ownership without losing recoverable documents | PASS — focused ownership/migration 17/17; manual and AI revisions plus source/action metadata survive adoption, downgrade and re-upgrade; owner/job/version index exists; deleting the parent application cascades through history; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 733/733 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Sixteen reconciler-owned tables remain | JT-019 cover-letter history transfer complete; interview-preparation items and career-profile aggregate continue incrementally | | V-205 | Ownership/migration chain; populated adoption/downgrade/retry; document/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move append-only cover-letter revision history to migration ownership without losing recoverable documents | PASS — focused ownership/migration 17/17; manual and AI revisions plus source/action metadata survive adoption, downgrade and re-upgrade; owner/job/version index exists; deleting the parent application cascades through history; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 733/733 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Sixteen reconciler-owned tables remain | JT-019 cover-letter history transfer complete; interview-preparation items and career-profile aggregate continue incrementally |
| V-206 | Ownership/migration chain; populated adoption/downgrade/retry; practice-state/FK/index assertions; MariaDB script; full backend | Repository root / disposable local SQLite | Move durable interview-preparation items to migration ownership without losing user or AI preparation content | PASS — focused ownership/migration 18/18; user-authored and AI-generated questions, answers, prepared state and source metadata survive adoption, downgrade and re-upgrade; owner/job/sort index exists; deleting the parent application cascades through preparation items; generated MariaDB SQL is provider-safe; startup no longer creates the table; full backend 734/734 | MariaDB SQL generated only; no provider account or production migration. Guarded malformed-empty/index/auto-increment repair remains temporarily. Fifteen reconciler-owned tables remain | JT-019 feature-table transfers complete; the Career Profile aggregate and Identity group remain dependency-aware batches |
+7 -4
View File
@@ -70,7 +70,7 @@ Created by EF migrations, never by the reconciler:
`ApplicationChecklistItems`, `Attachments`, `Companies`, `CoverLetterVersions`, `ApplicationChecklistItems`, `Attachments`, `Companies`, `CoverLetterVersions`,
`Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `CvVariants`, `CvVariantVersions`, `Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `CvVariants`, `CvVariantVersions`,
`EmailDrafts`, `EmailSendAttempts`, `GmailConnections`, `EmailDrafts`, `EmailSendAttempts`, `GmailConnections`,
`GmailReviewDecisions`, `ImapConnections`, `InterviewPrepNotes`, `JobApplications`, `JobEvents`, `Jobs`, `GmailReviewDecisions`, `ImapConnections`, `InterviewPrepItems`, `InterviewPrepNotes`, `JobApplications`, `JobEvents`, `Jobs`,
`MicrosoftGraphConnections`, `RuleSettings`, `SystemEmailSettings`, `TailoredCvDrafts`, `MicrosoftGraphConnections`, `RuleSettings`, `SystemEmailSettings`, `TailoredCvDrafts`,
`TrustedDevices`, `TwoFactorRecoveryCodes`, `UserNotifications`, `UserOperations`, `UserRuleSettings`, `TrustedDevices`, `TwoFactorRecoveryCodes`, `UserNotifications`, `UserOperations`, `UserRuleSettings`,
and `UserSessions`. and `UserSessions`.
@@ -123,6 +123,10 @@ manual tasks; application deletion remains cascading.
revisions retain their source/action metadata and owner/job/version ordering; application deletion revisions retain their source/action metadata and owner/job/version ordering; application deletion
remains cascading. remains cascading.
`InterviewPrepItems` moved in `20260830132000_AdoptInterviewPrepItemSchema`. User-authored and
AI-generated questions, answers, preparation state, sources, and ordering are retained; application
deletion remains cascading.
The reconciler may **repair** these (add a missing column, add an index, fix a non-`AUTO_INCREMENT` 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. 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 It used to create `RuleSettings`, which is precisely why a clean install failed with
@@ -133,8 +137,7 @@ It used to create `RuleSettings`, which is precisely why a clean install failed
Created by `StartupInitializationExtensions`, with a **no-op migration** holding the model snapshot: Created by `StartupInitializationExtensions`, with a **no-op migration** holding the model snapshot:
`CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`, `CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`,
`CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`), `CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`).
`InterviewPrepItems`.
The seven ASP.NET Identity tables are also currently reconciler-owned, despite older wording that The seven ASP.NET Identity tables are also currently reconciler-owned, despite older wording that
called them migration-owned: `AspNetRoles`, `AspNetUsers`, `AspNetRoleClaims`, `AspNetUserClaims`, called them migration-owned: `AspNetRoles`, `AspNetUsers`, `AspNetRoleClaims`, `AspNetUserClaims`,
@@ -156,7 +159,7 @@ No-op migrations, each with a comment explaining why:
| `20260719085904_AddApplicationChecklistItems` | historical no-op; ownership transferred by `20260830130000_AdoptApplicationChecklistSchema` | | `20260719085904_AddApplicationChecklistItems` | historical no-op; ownership transferred by `20260830130000_AdoptApplicationChecklistSchema` |
| `20260719094728_SyncCareerChildKeyLengths` | snapshot sync only | | `20260719094728_SyncCareerChildKeyLengths` | snapshot sync only |
| `20260719120954_AddCoverLetterVersions` | historical no-op; ownership transferred by `20260830131000_AdoptCoverLetterVersionSchema` | | `20260719120954_AddCoverLetterVersions` | historical no-op; ownership transferred by `20260830131000_AdoptCoverLetterVersionSchema` |
| `20260719145044_AddInterviewPrepItems` | `InterviewPrepItems` | | `20260719145044_AddInterviewPrepItems` | historical no-op; ownership transferred by `20260830132000_AdoptInterviewPrepItemSchema` |
### Dependency guards ### Dependency guards
+7 -2
View File
@@ -39,6 +39,8 @@ cohesive table groups. It does not attempt to delete the legacy reconciler whole
stable system keys, manual steps, completion state, and user ordering. stable system keys, manual steps, completion state, and user ordering.
- Added `20260830131000_AdoptCoverLetterVersionSchema` for append-only document history, retaining - Added `20260830131000_AdoptCoverLetterVersionSchema` for append-only document history, retaining
manual and AI-approved text plus source/action metadata. manual and AI-approved text plus source/action metadata.
- Added `20260830132000_AdoptInterviewPrepItemSchema` for durable interview-practice content,
retaining user and AI questions, answers, prepared state, source metadata, and ordering.
- Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy - Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy
compatibility code to retire one dependency group at a time. compatibility code to retire one dependency group at a time.
@@ -76,14 +78,17 @@ migration.
re-upgrade; deleting the parent application still cascades through its workflow items. re-upgrade; deleting the parent application still cascades through its workflow items.
- Representative manual and AI cover-letter revisions survive adoption, downgrade, and re-upgrade; - Representative manual and AI cover-letter revisions survive adoption, downgrade, and re-upgrade;
deleting the parent application still cascades through document history. deleting the parent application still cascades through document history.
- Representative user-authored and AI-generated interview-preparation items survive adoption,
downgrade, and re-upgrade with practice state intact; their ordering index and application cascade
remain effective.
- Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL. - Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL.
- Full backend: 733/733 passed after the cover-letter history transfer. - Full backend: 734/734 passed after the interview-preparation transfer.
- Fresh application startup over a new disposable SQLite database applied - Fresh application startup over a new disposable SQLite database applied
`20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state. `20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state.
## Remaining JT-019 work ## Remaining JT-019 work
Sixteen model tables remain startup-created, including the Identity group and several tables Fifteen model tables remain startup-created: the Career Profile aggregate and Identity group.
with parent dependencies. Transfer them in small dependency-aware migrations with blank, populated, 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 retry and MariaDB runtime proof. Column/index repairs must remain until historical upgrade fixtures
prove each one redundant. prove each one redundant.
+2 -1
View File
@@ -55,6 +55,7 @@ Updated: 2026-08-30
- Moved append-only AI interaction history into a provider-aware migration; generated results, modes, providers, usage counters, indexes, and application cascades are preserved. - Moved append-only AI interaction history into a provider-aware migration; generated results, modes, providers, usage counters, indexes, and application cascades are preserved.
- Moved application checklist workflow state into a provider-aware migration; system-key idempotency, manual tasks, ordering, completion state, and application cascades are preserved. - Moved application checklist workflow state into a provider-aware migration; system-key idempotency, manual tasks, ordering, completion state, and application cascades are preserved.
- Moved append-only cover-letter revision history into a provider-aware migration; manual and AI text, source/action metadata, ordering, and application cascades are preserved. - Moved append-only cover-letter revision history into a provider-aware migration; manual and AI text, source/action metadata, ordering, and application cascades are preserved.
- Moved durable interview-preparation items into a provider-aware migration; user and AI content, answers, practice state, sources, ordering, and application cascades are preserved.
### In progress ### In progress
@@ -85,7 +86,7 @@ Updated: 2026-08-30
- Focused frontend: 2 suites, 6 tests passed. - Focused frontend: 2 suites, 6 tests passed.
- Full frontend: 64 suites, 272 tests passed. - Full frontend: 64 suites, 272 tests passed.
- Next production build and TypeScript: passed. - Next production build and TypeScript: passed.
- Full backend: 733/733 tests passed after the cover-letter history JT-019 transfer. - Full backend: 734/734 tests passed after the interview-preparation JT-019 transfer.
- Portable Playwright launcher: resolved the user-local .NET 9 SDK; backend Release build passed with 0 warnings/errors. - 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. - 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. - Focused backend match/intelligence verification: 34/34 passed.