refactor(db): migrate application checklist

Move workflow checklist state into an additive provider-aware migration. Preserve system-key idempotency, manual progress, ordering, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:52:08 +02:00
parent ae57d1afde
commit e933409698
9 changed files with 165 additions and 63 deletions
@@ -115,6 +115,7 @@ public sealed class MigrationChainTests
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("CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -703,6 +704,68 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Checklist_adoption_preserves_system_and_manual_workflow_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("20260830129000_AdoptAiInteractionSchema");
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 "ApplicationChecklistItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApplicationChecklistItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "JobApplicationId" INTEGER NOT NULL,
"SystemKey" TEXT NULL, "AutoSignal" TEXT NULL, "Title" TEXT NOT NULL,
"Description" TEXT NULL, "Category" TEXT NOT NULL, "Status" TEXT NOT NULL,
"Section" TEXT NULL, "SortOrder" INTEGER NOT NULL,
"IsSystemGenerated" INTEGER NOT NULL, "IsAutoCompleted" INTEGER NOT NULL,
"CompletedAt" TEXT NULL, "CreatedAtUtc" TEXT NOT NULL, "UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_ApplicationChecklistItems_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
INSERT INTO "ApplicationChecklistItems"
("OwnerUserId", "JobApplicationId", "SystemKey", "AutoSignal", "Title",
"Category", "Status", "Section", "SortOrder", "IsSystemGenerated",
"IsAutoCompleted", "CompletedAt", "CreatedAtUtc", "UpdatedAtUtc")
VALUES
('owner-fixture', 1, 'prepare-cv', 'cv-ready', 'Prepare CV', 'preparation', 'done',
'cv', 10, 1, 1, '2026-08-30T09:10:00+00:00',
'2026-08-30T09:00:00+00:00', '2026-08-30T09:10:00+00:00'),
('owner-fixture', 1, NULL, NULL, 'Call recruiter', 'custom', 'pending',
NULL, 20, 0, 0, NULL, '2026-08-30T09:00:00+00:00',
'2026-08-30T09:00:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("done", await ScalarAsync<string>(connection,
"SELECT Status FROM ApplicationChecklistItems WHERE SystemKey = 'prepare-cv';"));
Assert.Equal("Call recruiter", await ScalarAsync<string>(connection,
"SELECT Title FROM ApplicationChecklistItems WHERE SystemKey IS NULL;"));
await migrator.MigrateAsync("20260830129000_AdoptAiInteractionSchema");
Assert.Equal(2L, await ScalarAsync<long>(connection,
"SELECT COUNT(*) FROM ApplicationChecklistItems WHERE JobApplicationId = 1;"));
await migrator.MigrateAsync();
Assert.Equal(2L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name IN (
'IX_ApplicationChecklistItems_JobApplicationId_SystemKey',
'IX_ApplicationChecklistItems_Owner_Job_Sort');
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM ApplicationChecklistItems;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();
@@ -0,0 +1,84 @@
using System;
using JobTrackerApi.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace JobTrackerApi.Migrations;
/// <summary>
/// Moves the per-application workflow checklist into migration ownership while preserving stable
/// system keys, manual items, ordering, and completion state.
/// </summary>
[DbContext(typeof(JobTrackerContext))]
[Migration("20260830130000_AdoptApplicationChecklistSchema")]
public sealed class AdoptApplicationChecklistSchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (ActiveProvider.Contains("MySql", StringComparison.OrdinalIgnoreCase))
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`SystemKey` varchar(64) NULL,
`AutoSignal` varchar(64) NULL,
`Title` varchar(255) NOT NULL,
`Description` longtext NULL,
`Category` varchar(32) NOT NULL,
`Status` varchar(32) NOT NULL,
`Section` varchar(64) NULL,
`SortOrder` int NOT NULL,
`IsSystemGenerated` tinyint(1) NOT NULL,
`IsAutoCompleted` tinyint(1) NOT NULL,
`CompletedAt` datetime(6) NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_ApplicationChecklistItems_JobApplications_JobApplicationId`
FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS `IX_ApplicationChecklistItems_JobApplicationId_SystemKey`
ON `ApplicationChecklistItems` (`JobApplicationId`, `SystemKey`);
CREATE INDEX IF NOT EXISTS `IX_ApplicationChecklistItems_Owner_Job_Sort`
ON `ApplicationChecklistItems` (`OwnerUserId`, `JobApplicationId`, `SortOrder`);
""");
return;
}
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "ApplicationChecklistItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApplicationChecklistItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"SystemKey" TEXT NULL,
"AutoSignal" TEXT NULL,
"Title" TEXT NOT NULL,
"Description" TEXT NULL,
"Category" TEXT NOT NULL,
"Status" TEXT NOT NULL,
"Section" TEXT NULL,
"SortOrder" INTEGER NOT NULL,
"IsSystemGenerated" INTEGER NOT NULL,
"IsAutoCompleted" INTEGER NOT NULL,
"CompletedAt" TEXT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_ApplicationChecklistItems_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "IX_ApplicationChecklistItems_JobApplicationId_SystemKey"
ON "ApplicationChecklistItems" ("JobApplicationId", "SystemKey");
CREATE INDEX IF NOT EXISTS "IX_ApplicationChecklistItems_Owner_Job_Sort"
ON "ApplicationChecklistItems" ("OwnerUserId", "JobApplicationId", "SortOrder");
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// Preserve workflow decisions that may pre-date explicit migration ownership.
}
}
@@ -712,34 +712,6 @@ public static class StartupInitializationExtensions
EnsureColumn(c, "AiInteractions", "EstimatedTokenCount", "ALTER TABLE AiInteractions ADD COLUMN EstimatedTokenCount INTEGER NOT NULL DEFAULT 0;");
}
// Phase 5 Milestone 2: the application checklist (workflow guidance over readiness signals).
static void EnsureApplicationChecklistTable(DbConnection c)
{
Exec(c, """
CREATE TABLE IF NOT EXISTS "ApplicationChecklistItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApplicationChecklistItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL,
"JobApplicationId" INTEGER NOT NULL,
"SystemKey" TEXT NULL,
"AutoSignal" TEXT NULL,
"Title" TEXT NOT NULL,
"Description" TEXT NULL,
"Category" TEXT NOT NULL,
"Status" TEXT NOT NULL,
"Section" TEXT NULL,
"SortOrder" INTEGER NOT NULL,
"IsSystemGenerated" INTEGER NOT NULL,
"IsAutoCompleted" INTEGER NOT NULL,
"CompletedAt" TEXT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_ApplicationChecklistItems_JobApplications_JobApplicationId" FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
""");
Exec(c, """CREATE UNIQUE INDEX IF NOT EXISTS "IX_ApplicationChecklistItems_JobApplicationId_SystemKey" ON "ApplicationChecklistItems" ("JobApplicationId", "SystemKey");""");
Exec(c, """CREATE INDEX IF NOT EXISTS "IX_ApplicationChecklistItems_Owner_Job_Sort" ON "ApplicationChecklistItems" ("OwnerUserId", "JobApplicationId", "SortOrder");""");
}
// Phase 5.4: append-only cover letter history.
static void EnsureCoverLetterVersionsTable(DbConnection c)
{
@@ -784,7 +756,6 @@ public static class StartupInitializationExtensions
ReconcileGmailConnectionColumns(conn);
EnsureCareerProfileTables(conn);
ReconcileAiInteractionUsageColumns(conn);
EnsureApplicationChecklistTable(conn);
EnsureCoverLetterVersionsTable(conn);
EnsureInterviewPrepItemsTable(conn);
@@ -1180,32 +1151,6 @@ public static class StartupInitializationExtensions
DropMalformedMySqlTable(conn, "CoverLetterVersions", "CreatedAtUtc", "datetime");
DropMalformedMySqlTable(conn, "InterviewPrepItems", "CreatedAtUtc", "datetime");
if (!HasMySqlTable(conn, "ApplicationChecklistItems") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems` (
`Id` int NOT NULL AUTO_INCREMENT,
`OwnerUserId` varchar(255) NOT NULL,
`JobApplicationId` int NOT NULL,
`SystemKey` varchar(64) NULL,
`AutoSignal` varchar(64) NULL,
`Title` varchar(255) NOT NULL,
`Description` longtext NULL,
`Category` varchar(32) NOT NULL,
`Status` varchar(32) NOT NULL,
`Section` varchar(64) NULL,
`SortOrder` int NOT NULL,
`IsSystemGenerated` tinyint(1) NOT NULL,
`IsAutoCompleted` tinyint(1) NOT NULL,
`CompletedAt` datetime(6) NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`UpdatedAtUtc` datetime(6) NOT NULL,
PRIMARY KEY (`Id`),
CONSTRAINT `FK_ApplicationChecklistItems_JobApplications_JobApplicationId` FOREIGN KEY (`JobApplicationId`) REFERENCES `JobApplications` (`Id`) ON DELETE CASCADE
);";
cmd.ExecuteNonQuery();
}
if (!HasMySqlTable(conn, "CoverLetterVersions") && HasMySqlTable(conn, "JobApplications"))
{
using var cmd = conn.CreateCommand();
@@ -14,6 +14,7 @@ internal static class StartupSchemaOwnership
"AiUsageRecords",
"AiWorkspaceNotes",
"AiInteractions",
"ApplicationChecklistItems",
"Attachments",
"Companies",
"Correspondences",
@@ -44,7 +45,6 @@ internal static class StartupSchemaOwnership
internal static readonly IReadOnlySet<string> ReconcilerOwnedTables = new HashSet<string>(StringComparer.Ordinal)
{
"ApplicationChecklistItems",
"AspNetRoleClaims",
"AspNetRoles",
"AspNetUserClaims",
+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. Seventeen formerly reconciler-created tables—including authentication support, email-provider connections, CV extraction persistence, tailored CV drafts, job-workspace AI notes, CV variant history, and append-only AI interactions—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. Eighteen startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-203.
**Status (2026-08-30): in progress.** All 49 model tables now have an executable, disjoint creation-owner classification. Eighteen formerly reconciler-created tables—including authentication support, email-provider connections, CV persistence/history, job-workspace AI notes, append-only AI interactions, and application workflow checklists—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. Seventeen startup-created tables remain; see `docs/verification/jt-019-schema-ownership.md` and V-194V-204.
- **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
@@ -235,3 +235,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
| 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 |
| 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 |
+7 -3
View File
@@ -67,7 +67,7 @@ already-correct database. Two consequences worth knowing:
Created by EF migrations, never by the reconciler:
`AccountDeletionFiles`, `AccountDeletionRequests`, `AiInteractions`, `AiUsageRecords`, `AiWorkspaceNotes`,
`Attachments`, `Companies`,
`ApplicationChecklistItems`, `Attachments`, `Companies`,
`Correspondences`, `CvExtractionRuns`, `CvUploadArtifacts`, `CvVariants`, `CvVariantVersions`,
`EmailDrafts`, `EmailSendAttempts`, `GmailConnections`,
`GmailReviewDecisions`, `ImapConnections`, `InterviewPrepNotes`, `JobApplications`, `JobEvents`, `Jobs`,
@@ -115,6 +115,10 @@ sets the CV link to null, and deleting a CV cascades through its append-only rev
migration retains its compatibility bootstrap for historical chain traversal, but startup no longer
creates the table; only additive counter and guarded MariaDB shape/index repairs remain.
`ApplicationChecklistItems` moved in `20260830130000_AdoptApplicationChecklistSchema`. Its stable
system-key uniqueness and owner/job/sort index preserve idempotent seeding alongside freely ordered
manual tasks; application deletion remains cascading.
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
@@ -126,7 +130,7 @@ Created by `StartupInitializationExtensions`, with a **no-op migration** holding
`CareerProfiles`, `CareerProfileVersions`, the six CareerProfile children (`CareerExperiences`,
`CareerEducations`, `CareerSkills`, `CareerProjects`, `CareerCertifications`, `CareerLanguages`),
`ApplicationChecklistItems`, `CoverLetterVersions`, and `InterviewPrepItems`.
`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`,
@@ -145,7 +149,7 @@ No-op migrations, each with a comment explaining why:
| `20260717222917_AddCareerProfileRelationalChildren` | the six CareerProfile children |
| `20260718074509_AddCvVariants` | historical no-op; ownership transferred by `20260830128000_AdoptCvVariantSchema` |
| `20260718131138_AddAiInteractions` | historical no-op; ownership transferred by `20260830129000_AdoptAiInteractionSchema` |
| `20260719085904_AddApplicationChecklistItems` | `ApplicationChecklistItems` |
| `20260719085904_AddApplicationChecklistItems` | historical no-op; ownership transferred by `20260830130000_AdoptApplicationChecklistSchema` |
| `20260719094728_SyncCareerChildKeyLengths` | snapshot sync only |
| `20260719120954_AddCoverLetterVersions` | `CoverLetterVersions` |
| `20260719145044_AddInterviewPrepItems` | `InterviewPrepItems` |
+6 -2
View File
@@ -35,6 +35,8 @@ cohesive table groups. It does not attempt to delete the legacy reconciler whole
retaining public slugs, builder settings, and job-link/version retention semantics.
- Added `20260830129000_AdoptAiInteractionSchema` to take ownership from both the startup reconciler
and historical compatibility bootstrap while retaining append-only results and usage counters.
- Added `20260830130000_AdoptApplicationChecklistSchema` for application workflow state, retaining
stable system keys, manual steps, completion state, and user ordering.
- Corrected the ownership runbook: new tables now default to migrations; the reconciler is legacy
compatibility code to retire one dependency group at a time.
@@ -68,14 +70,16 @@ migration.
its job clears only the optional link, while deleting the CV cascades through its revisions.
- A representative AI result, mode, provider, and usage counts survive adoption, downgrade, and
re-upgrade; deleting the parent application still cascades through its interaction history.
- Representative system-generated and manual checklist steps survive adoption, downgrade, and
re-upgrade; deleting the parent application still cascades through its workflow items.
- Generated MariaDB SQL contains the provider-correct `SystemEmailSettings` DDL.
- Full backend: 731/731 passed after the AI interaction history transfer.
- Full backend: 732/732 passed after the application checklist transfer.
- Fresh application startup over a new disposable SQLite database applied
`20260830120000_AdoptSystemEmailSettingsSchema` and reached the healthy listening state.
## Remaining JT-019 work
Eighteen model tables remain startup-created, including the Identity group and several tables
Seventeen 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
@@ -53,6 +53,7 @@ Updated: 2026-08-30
- 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.
- 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.
### In progress
@@ -83,7 +84,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: 731/731 tests passed after the AI interaction history JT-019 transfer.
- Full backend: 732/732 tests passed after the application checklist 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.