diff --git a/JobTrackerApi.Tests/DocumentsControllerTests.cs b/JobTrackerApi.Tests/DocumentsControllerTests.cs new file mode 100644 index 0000000..e6e2234 --- /dev/null +++ b/JobTrackerApi.Tests/DocumentsControllerTests.cs @@ -0,0 +1,546 @@ +using System.Data.Common; +using System.Net; +using System.Reflection; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.RegularExpressions; +using JobTrackerApi.Controllers; +using JobTrackerApi.Data; +using JobTrackerApi.Models; +using JobTrackerApi.Services; +using JobTrackerApi.Tests.TestSupport; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace JobTrackerApi.Tests; + +public sealed class DocumentsControllerTests +{ + private const string OwnerA = "documents-owner-a"; + private const string OwnerB = "documents-owner-b"; + private const string PrivateContent = "NEVER_EXPOSE_DOCUMENT_CONTENT"; + private static readonly string[] ItemFields = + [ + "id", "resourceId", "type", "name", "state", "visibility", "jobApplicationId", "jobTitle", + "companyName", "jobIsDeleted", "recordedAt", "dateKind", "version", "historyCount", + "attachmentCount", "cvVariantId", "cvVariantName", "cvVariantVersion", "fileType", "fileSize", + "purpose", "useForAi", "provider", "sourceHref", "downloadHref", "publicHref", "hasCurrentContent", + ]; + + [Fact] + public async Task Empty_index_has_bounded_defaults_and_requires_a_known_authenticated_owner() + { + await using var fixture = await Fixture.CreateAsync(); + var page = await fixture.GetAsync(); + Assert.Equal(new[] { "items", "page", "pageSize", "total", "totalPages" }.Order(), + page.EnumerateObject().Select(property => property.Name).Order()); + Assert.Empty(Items(page)); + Assert.Equal(1, page.GetProperty("page").GetInt32()); + Assert.Equal(25, page.GetProperty("pageSize").GetInt32()); + Assert.Equal(0, page.GetProperty("total").GetInt32()); + Assert.Equal(0, page.GetProperty("totalPages").GetInt32()); + Assert.Equal(HttpStatusCode.Unauthorized, await fixture.StatusAsync("", null)); + Assert.Equal(HttpStatusCode.Unauthorized, await fixture.StatusAsync("", "unrecognised-owner")); + } + + [Fact] + public async Task Seven_existing_resource_kinds_expose_only_metadata_and_existing_actions() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + var page = await fixture.GetAsync(); + var items = Items(page); + Assert.Equal(8, page.GetProperty("total").GetInt32()); + Assert.Equal(new[] { "application-material", "attachment", "cover-letter", "cv", "email-draft", "submitted-package", "tailored-cv" }, + items.Select(item => Text(item, "type")).Distinct().Order()); + foreach (var item in items) + { + Assert.Equal(ItemFields.Order(), item.EnumerateObject().Select(property => property.Name).Order()); + Assert.Equal($"{Text(item, "type")}:{Text(item, "resourceId")}", Text(item, "id")); + Assert.StartsWith("/", Text(item, "sourceHref")); + } + var cv = items.Single(item => Text(item, "id") == "cv:10"); + Assert.Equal("Public ÆØÅ CV", Text(cv, "name")); + Assert.Equal("current", Text(cv, "state")); + Assert.Equal("public", Text(cv, "visibility")); + Assert.Equal("/career/builder/10", Text(cv, "sourceHref")); + Assert.Equal("/cv/documents-public-a", Text(cv, "publicHref")); + Assert.Equal(2, cv.GetProperty("version").GetInt64()); + Assert.Null(Text(cv, "downloadHref")); + var upload = items.Single(item => Text(item, "type") == "attachment"); + Assert.Equal("/api/attachments/download/1", Text(upload, "downloadHref")); + Assert.Equal("portfolio", Text(upload, "purpose")); + Assert.Equal("application/pdf", Text(upload, "fileType")); + Assert.Equal(41, upload.GetProperty("fileSize").GetInt64()); + Assert.False(upload.GetProperty("useForAi").GetBoolean()); + Assert.Equal("uploaded-local", Text(upload, "dateKind")); + Assert.Equal("2026-01-02T03:04:05.1234567", Text(upload, "recordedAt")); + var email = items.Single(item => Text(item, "type") == "email-draft"); + Assert.Equal("Saved subject ÆØÅ", Text(email, "name")); + Assert.Equal("gmail", Text(email, "provider")); + Assert.Equal("draft", Text(email, "state")); + Assert.Contains("/correspondence", Text(email, "sourceHref")); + var letter = items.Single(item => Text(item, "type") == "cover-letter"); + Assert.Equal("draft", Text(letter, "state")); + Assert.True(letter.GetProperty("hasCurrentContent").GetBoolean()); + Assert.Equal(2, letter.GetProperty("historyCount").GetInt32()); + Assert.Equal(2, letter.GetProperty("version").GetInt64()); + Assert.Equal("history-recorded-utc", Text(letter, "dateKind")); + Assert.DoesNotContain(PrivateContent, page.GetRawText()); + Assert.DoesNotContain("private-recipient@example.test", page.GetRawText()); + Assert.DoesNotContain("private-thread-a", page.GetRawText()); + Assert.DoesNotContain("private-client-request-a", page.GetRawText()); + Assert.DoesNotContain("private-storage", page.GetRawText()); + } + + [Fact] + public async Task Owner_scope_includes_owned_history_and_trash_without_leaking_cross_owner_references() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + await fixture.SeedAsync(db => + { + db.JobApplications.Add(new JobApplication { Id = 3, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Owned trash", IsDeleted = true, CoverLetterText = PrivateContent }); + db.CvVariants.Add(Variant(12, OwnerA, "Malformed association", 2)); + db.CoverLetterVersions.Add(new CoverLetterVersion { OwnerUserId = OwnerB, JobApplicationId = 1, Version = 99, Text = PrivateContent }); + db.EmailDrafts.Add(Draft(Guid.Parse("33333333-3333-3333-3333-333333333333"), OwnerA, 2, "Malformed cross-owner draft")); + db.SubmittedApplicationPackages.Add(new SubmittedApplicationPackage { OwnerUserId = OwnerA, JobApplicationId = 2, Version = 99, CvVariantName = "Malformed cross-owner capture" }); + db.JobApplications.AddRange( + new JobApplication { Id = 4, OwnerUserId = OwnerA, CompanyId = 2, JobTitle = "Owned role with damaged company reference", CoverLetterText = PrivateContent }, + new JobApplication { Id = 5, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Owned role with foreign structured draft" }); + db.TailoredCvDrafts.Add(new TailoredCvDraft { OwnerUserId = OwnerB, JobApplicationId = 5, SummaryJson = $"[\"{PrivateContent}\"]", LastEditedAtUtc = DateTimeOffset.Parse("2026-01-01T00:00:00Z") }); + }); + var own = Items(await fixture.GetAsync("?pageSize=100")); + Assert.Contains(own, item => Text(item, "name") == "Owned trash" && item.GetProperty("jobIsDeleted").GetBoolean()); + Assert.DoesNotContain(own, item => Text(item, "name") == "Foreign CV" || Text(item, "companyName") == "Foreign company"); + Assert.DoesNotContain(own, item => Text(item, "name") is "Malformed cross-owner draft" or "Malformed cross-owner capture"); + var malformedCv = own.Single(item => Text(item, "id") == "cv:12"); + Assert.Equal(JsonValueKind.Null, malformedCv.GetProperty("jobApplicationId").ValueKind); + Assert.Null(Text(malformedCv, "jobTitle")); + Assert.Null(Text(malformedCv, "companyName")); + var damagedCompany = own.Single(item => Text(item, "id") == "cover-letter:4"); + Assert.Equal(4, damagedCompany.GetProperty("jobApplicationId").GetInt32()); + Assert.Null(Text(damagedCompany, "companyName")); + Assert.DoesNotContain(own, item => Text(item, "id") == "tailored-cv:5"); + Assert.Equal(2, own.Single(item => Text(item, "id") == "cover-letter:1").GetProperty("historyCount").GetInt32()); + Assert.Equal(own.Select(item => Text(item, "id")), Items(await fixture.GetAsync($"?pageSize=100&ownerUserId={OwnerB}")).Select(item => Text(item, "id"))); + Assert.Empty(Items(await fixture.GetAsync("?jobApplicationId=2"))); + Assert.Empty(Items(await fixture.GetAsync("?jobApplicationId=99999"))); + var other = Items(await fixture.GetAsync("?pageSize=100", OwnerB)); + Assert.Equal(7, other.Length); + Assert.All(other, item => Assert.Equal(2, item.GetProperty("jobApplicationId").GetInt32())); + Assert.DoesNotContain(PrivateContent, JsonSerializer.Serialize(other)); + } + + [Fact] + public async Task Pagination_counts_the_filtered_result_and_uses_stable_ordinal_resource_ties() + { + await using var fixture = await Fixture.CreateAsync(); + await fixture.SeedAsync(db => db.CvVariants.AddRange(Enumerable.Range(1, 70).Select(id => Variant(id, OwnerA, $"CV {id:D3}")))); + var first = await fixture.GetAsync("?type=cv&sort=date-desc"); + var second = await fixture.GetAsync("?type=cv&sort=date-desc&page=2"); + Assert.Equal(70, first.GetProperty("total").GetInt32()); + Assert.Equal(3, first.GetProperty("totalPages").GetInt32()); + Assert.Equal(25, Items(first).Length); + Assert.Equal(25, Items(second).Length); + var orderedIds = Enumerable.Range(1, 70).Select(id => id.ToString()).Order(StringComparer.Ordinal).ToArray(); + Assert.Equal(orderedIds.Take(25), Items(first).Select(item => Text(item, "resourceId"))); + Assert.Equal(orderedIds.Skip(25).Take(25), Items(second).Select(item => Text(item, "resourceId"))); + var last = await fixture.GetAsync("?type=cv&sort=name-desc&page=3"); + Assert.Equal(Enumerable.Range(1, 20).Reverse().Select(id => $"CV {id:D3}"), Items(last).Select(item => Text(item, "name"))); + var all = await fixture.GetAsync("?type=cv&pageSize=100"); + Assert.Equal(70, Items(all).Length); + Assert.Empty(Items(await fixture.GetAsync("?type=cv&page=10000"))); + } + + [Fact] + public async Task Type_state_visibility_and_owned_job_filters_intersect_without_changing_totals() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + Assert.Equal(4, (await fixture.GetAsync("?state=draft")).GetProperty("total").GetInt32()); + Assert.Equal(3, (await fixture.GetAsync("?state=current")).GetProperty("total").GetInt32()); + Assert.Equal(1, (await fixture.GetAsync("?state=frozen")).GetProperty("total").GetInt32()); + var publicOnly = Items(await fixture.GetAsync("?visibility=public")); + Assert.Equal("cv:10", Text(Assert.Single(publicOnly), "id")); + Assert.Equal("cv:11", Text(Assert.Single(Items(await fixture.GetAsync("?visibility=private"))), "id")); + Assert.Empty(Items(await fixture.GetAsync("?type=attachment&visibility=public"))); + Assert.Equal(7, (await fixture.GetAsync("?jobApplicationId=1")).GetProperty("total").GetInt32()); + var scopedCv = await fixture.GetAsync("?type=cv&state=current&jobApplicationId=1&pageSize=1"); + Assert.Equal(1, scopedCv.GetProperty("total").GetInt32()); + Assert.Equal("cv:10", Text(Assert.Single(Items(scopedCv)), "id")); + } + + [Fact] + public async Task Search_treats_wildcards_literally_folds_Nordic_metadata_and_never_searches_content() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + await fixture.SeedAsync(db => db.CvVariants.AddRange( + Variant(31, OwnerA, "Literal 100%"), Variant(32, OwnerA, "Literal under_score"), + Variant(33, OwnerA, "Literal bang!"), Variant(34, OwnerA, "Literal decoy"))); + foreach (var (query, expected) in new[] { ("%", "cv:31"), ("_", "cv:32"), ("!", "cv:33") }) + Assert.Equal(expected, Text(Assert.Single(Items(await fixture.GetAsync($"?q={Uri.EscapeDataString(query)}"))), "id")); + Assert.Equal(new[] { "cv:10", "email-draft:11111111-1111-1111-1111-111111111111", "submitted-package:1" }, + Items(await fixture.GetAsync($"?q={Uri.EscapeDataString("æøå")}&sort=name-asc")).Select(item => Text(item, "id")).Order()); + Assert.Equal(7, (await fixture.GetAsync("?q=nordlys")).GetProperty("total").GetInt32()); + foreach (var query in new[] { PrivateContent, "private-recipient", "private-client-request", "private-thread", "private-storage" }) + Assert.Empty(Items(await fixture.GetAsync($"?q={Uri.EscapeDataString(query)}"))); + } + + [Fact] + public async Task Date_order_uses_exact_instants_and_fractional_ticks_with_unzoned_and_unknown_dates_last() + { + await using var fixture = await Fixture.CreateAsync(); + var instant = DateTimeOffset.Parse("2026-02-03T04:05:06.1234567+00:00"); + await fixture.SeedAsync(db => + { + var first = Variant(1, OwnerA, "Exact instant"); + first.UpdatedAtUtc = instant; + var tied = Variant(10, OwnerA, "Offset tie"); + tied.UpdatedAtUtc = instant.ToOffset(TimeSpan.FromHours(3)); + var later = Variant(2, OwnerA, "One tick later"); + later.UpdatedAtUtc = instant.AddTicks(1).ToOffset(TimeSpan.FromHours(-4)); + db.CvVariants.AddRange(first, tied, later); + db.Attachments.Add(new Attachment { Id = 1, JobApplicationId = 1, FileName = "Future local upload", UploadDate = new DateTime(2099, 1, 1), FilePath = "private-storage", FileType = "text/plain" }); + db.JobApplications.Single(job => job.Id == 1).RecruiterMessageDraft = PrivateContent; + }); + var asc = Items(await fixture.GetAsync("?sort=date-asc")); + var desc = Items(await fixture.GetAsync("?sort=date-desc")); + Assert.Equal(new[] { "cv:1", "cv:10", "cv:2", "application-material:1", "attachment:1" }, asc.Select(item => Text(item, "id"))); + Assert.Equal(new[] { "cv:2", "cv:1", "cv:10", "application-material:1", "attachment:1" }, desc.Select(item => Text(item, "id"))); + Assert.Equal(instant.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'"), Text(asc[0], "recordedAt")); + Assert.Equal(instant.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'"), Text(asc[1], "recordedAt")); + Assert.Equal(JsonValueKind.Null, asc[3].GetProperty("recordedAt").ValueKind); + Assert.Equal("unknown", Text(asc[3], "dateKind")); + Assert.Equal("uploaded-local", Text(asc[4], "dateKind")); + } + + [Fact] + public async Task Invalid_parameters_are_rejected_before_any_catalog_query() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + foreach (var query in new[] + { + "?page=0", "?page=-1", "?page=10001", "?page=oops", "?pageSize=0", "?pageSize=101", "?pageSize=oops", + "?type=unknown", "?state=sent", "?visibility=shared", "?sort=unknown", "?jobApplicationId=0", + "?jobApplicationId=-1", "?jobApplicationId=oops", $"?q={new string('a', 201)}", + "?sort=date-desc%3BDROP%20TABLE%20CvVariants", + }) + { + fixture.Commands.Reset(); + Assert.Equal(HttpStatusCode.BadRequest, await fixture.StatusAsync(query)); + Assert.Empty(fixture.Commands.Snapshot()); + } + } + + [Fact] + public async Task Cleared_letter_history_answer_markers_and_structured_tailoring_keep_their_real_sources() + { + var whitespace = new string(Enumerable.Range(char.MinValue, char.MaxValue + 1) + .Select(value => (char)value).Where(char.IsWhiteSpace).ToArray()); + var emptyAnswer = $"<<>>{whitespace}<<>>"; + Assert.True(string.IsNullOrWhiteSpace(whitespace)); + Assert.Null(JobApplicationHelpers.ExtractSavedApplicationAnswerDraft(emptyAnswer)); + await using var fixture = await Fixture.CreateAsync(); + await fixture.SeedAsync(db => + { + var job = db.JobApplications.Single(job => job.Id == 1); + job.Notes = $"Ordinary notes {PrivateContent}"; + db.CoverLetterVersions.Add(new CoverLetterVersion { OwnerUserId = OwnerA, JobApplicationId = 1, Version = 4, Text = PrivateContent, CreatedAtUtc = DateTimeOffset.Parse("2026-02-03T04:00:00+02:00") }); + db.TailoredCvDrafts.Add(new TailoredCvDraft { OwnerUserId = OwnerA, JobApplicationId = 1, SummaryJson = $"[\"{PrivateContent}\"]", LastEditedAtUtc = DateTimeOffset.Parse("2026-02-04T01:00:00Z") }); + db.JobApplications.AddRange( + new JobApplication { Id = 3, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Legacy answers", Notes = $"Human notes\naPpLiCaTiOn AnSwEr DrAfT: {PrivateContent}" }, + new JobApplication { Id = 4, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Modern answers", Notes = $"<<>>\n{PrivateContent}\n<<>>" }, + new JobApplication { Id = 5, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Empty marker", Notes = "<<>>\n \n<<>>" }, + new JobApplication { Id = 6, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Unclosed marker", Notes = "<<>>not an answer" }, + new JobApplication { Id = 7, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Whitespace drafts", CoverLetterText = whitespace, RecruiterMessageDraft = whitespace, Notes = emptyAnswer }, + new JobApplication { Id = 8, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Empty modern marker takes precedence", Notes = $"<<>> \t <<>> Application answer draft: {PrivateContent}" }, + new JobApplication { Id = 9, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Strict modern marker casing", Notes = $"<<>>{PrivateContent}<<>>" }); + }); + var page = await fixture.GetAsync(); + var items = Items(page); + Assert.Equal(new[] { "application-material:3", "application-material:4", "cover-letter:1", "tailored-cv:1" }, + items.Select(item => Text(item, "id")).Order()); + Assert.Equal(4, items.Length); + var letter = items.Single(item => Text(item, "type") == "cover-letter"); + Assert.False(letter.GetProperty("hasCurrentContent").GetBoolean()); + Assert.Equal(1, letter.GetProperty("historyCount").GetInt32()); + Assert.Equal(4, letter.GetProperty("version").GetInt64()); + Assert.Equal("2026-02-03T02:00:00.0000000Z", Text(letter, "recordedAt")); + Assert.Equal(new[] { "application-material:3", "application-material:4" }, + items.Where(item => Text(item, "type") == "application-material").Select(item => Text(item, "id")).Order()); + Assert.Equal("tailored-cv:1", Text(items.Single(item => Text(item, "type") == "tailored-cv"), "id")); + Assert.DoesNotContain(PrivateContent, page.GetRawText()); + } + + [Fact] + public async Task Frozen_metadata_and_archived_bytes_survive_live_cv_upload_and_letter_changes() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + var before = Assert.Single(Items(await fixture.GetAsync("?type=submitted-package"))); + Assert.Equal("Frozen CV ÆØÅ", Text(before, "cvVariantName")); + Assert.Equal(10, before.GetProperty("cvVariantId").GetInt32()); + Assert.Equal(1, before.GetProperty("cvVariantVersion").GetInt32()); + Assert.Equal(1, before.GetProperty("attachmentCount").GetInt32()); + Assert.Equal("frozen", Text(before, "state")); + var archivedBytes = await File.ReadAllBytesAsync(fixture.ArchivedFile); + await fixture.SeedAsync(db => + { + db.JobApplications.Single(job => job.Id == 1).CoverLetterText = "Changed live letter"; + db.CvVariants.Remove(db.CvVariants.Single(cv => cv.Id == 10)); + db.Attachments.Remove(db.Attachments.Single(attachment => attachment.Id == 1)); + }); + File.Delete(fixture.LiveFile); + var after = Assert.Single(Items(await fixture.GetAsync("?type=submitted-package"))); + Assert.Equal(before.GetRawText(), after.GetRawText()); + Assert.Equal(archivedBytes, await File.ReadAllBytesAsync(fixture.ArchivedFile)); + Assert.Equal(Convert.ToHexString(SHA256.HashData(archivedBytes)).ToLowerInvariant(), fixture.ArchivedSha256); + Assert.DoesNotContain(PrivateContent, after.GetRawText()); + } + + [Fact] + public async Task Index_uses_native_count_and_bounded_page_queries_without_writes_or_content_projection() + { + await using var fixture = await Fixture.CreateAsync(catalog: true); + await fixture.SeedAsync(db => db.CvVariants.AddRange(Enumerable.Range(100, 120).Select(id => Variant(id, OwnerA, $"Large {id:D3}")))); + var before = SHA256.HashData(await File.ReadAllBytesAsync(fixture.DatabaseFile)); + fixture.Commands.Reset(); + var page = await fixture.GetAsync("?page=2&pageSize=7"); + Assert.Equal(7, Items(page).Length); + Assert.Equal(128, page.GetProperty("total").GetInt32()); + var commands = fixture.Commands.Snapshot(); + Assert.Equal(2, commands.Length); + Assert.All(commands, command => Assert.StartsWith("SELECT", command.TrimStart(), StringComparison.OrdinalIgnoreCase)); + Assert.Contains(commands, command => command.Contains("COUNT(", StringComparison.OrdinalIgnoreCase)); + Assert.Contains(commands, command => command.Contains("LIMIT", StringComparison.OrdinalIgnoreCase) && command.Contains("OFFSET", StringComparison.OrdinalIgnoreCase)); + foreach (var forbidden in new[] { "\"BodyText\"", "\"SettingsJson\"", "\"RenderedCvHtml\"", "\"CareerProfileJson\"", "\"ApplicationMaterialJson\"", "\"FilePath\"", "\"To\"", "\"ThreadId\"", "\"ClientRequestId\"" }) + Assert.All(commands, command => Assert.DoesNotContain(forbidden, command, StringComparison.OrdinalIgnoreCase)); + Assert.All(commands, command => Assert.False(Regex.IsMatch(command, + @"\b(?:BodyText|SettingsJson|RenderedCvHtml|CareerProfileJson|ApplicationMaterialJson|FilePath|To|ThreadId|ClientRequestId)\b", RegexOptions.IgnoreCase))); + Assert.Equal(before, SHA256.HashData(await File.ReadAllBytesAsync(fixture.DatabaseFile))); + } + + [Fact] + public void MariaDb_composition_keeps_unicode_markers_ordinal_and_paging_parameterized_without_connecting() + { + var service = typeof(AttachmentsController).Assembly.GetType("JobTrackerApi.Services.DocumentsIndexService")!; + var builder = service.GetMethod("BuildSql", BindingFlags.Static | BindingFlags.NonPublic)!; + var countSql = (string)builder.Invoke(null, [false, true, "date-desc"])!; + var pageSql = (string)builder.Invoke(null, [false, false, "date-desc"])!; + // Raw SQL composition cannot prove MariaDB execution; this check protects the declared Unicode/ordinal policy. + Assert.Contains("COLLATE utf8mb4_bin", countSql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("CONVERT(COALESCE(j.Notes, '') USING utf8mb4)", countSql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("LOCATE(", countSql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("TIMESTAMPDIFF(MICROSECOND", pageSql, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("STRFTIME", pageSql, StringComparison.OrdinalIgnoreCase); + + var commands = new CommandRecorder(); + var currentUser = Mock.Of(current => current.UserId == OwnerA); + var options = new DbContextOptionsBuilder() + .UseMySql("Server=127.0.0.1;Database=composition_only;User=fixture;Password=fixture", new MariaDbServerVersion(new Version(11, 0, 0))) + .AddInterceptors(commands).Options; + using var db = new JobTrackerContext(options, currentUser); + object[] parameters = [OwnerA, "%æøå!%!_!!%", "all", "all", "all", 0, 7, 14]; + var countComposition = db.Database.SqlQueryRaw(countSql, parameters).ToQueryString(); + var rowType = service.GetNestedType("DocumentRow", BindingFlags.NonPublic)!; + var queryMethod = typeof(RelationalDatabaseFacadeExtensions).GetMethods() + .Single(method => method.Name == "SqlQueryRaw" && method.IsGenericMethodDefinition && method.GetParameters().Length == 3); + var pageQuery = (IQueryable)queryMethod.MakeGenericMethod(rowType).Invoke(null, [db.Database, pageSql, parameters])!; + var pageComposition = pageQuery.ToQueryString(); + Assert.Contains("COUNT(*)", countComposition, StringComparison.OrdinalIgnoreCase); + Assert.Contains("@p0", countComposition, StringComparison.Ordinal); + // Pomelo's display query substitutes pagination values; inspect the actual unexecuted command for binding. + Assert.Matches(@"(?i)\bLIMIT\s+7\s+OFFSET\s+14\s*;?\s*$", pageComposition); + using var pageCommand = pageQuery.CreateDbCommand(); + var paging = Regex.Match(pageCommand.CommandText, @"\bLIMIT\s+(?@\w+)\s+OFFSET\s+(?@\w+)\s*;?\s*$", RegexOptions.IgnoreCase); + Assert.True(paging.Success, pageCommand.CommandText); + Assert.NotEqual(paging.Groups["limit"].Value, paging.Groups["offset"].Value); + foreach (var (name, value) in new[] { ("limit", 7), ("offset", 14) }) + { + var parameter = Assert.Single(pageCommand.Parameters.Cast(), + parameter => parameter.ParameterName.TrimStart('@') == paging.Groups[name].Value.TrimStart('@')); + Assert.Equal(value, Convert.ToInt32(parameter.Value)); + } + Assert.Contains(pageCommand.Parameters.Cast(), parameter => Equals(parameter.Value, OwnerA)); + Assert.DoesNotMatch(@"(?i)\bLIMIT\s+\d+\s+OFFSET\s+\d+\s*;?\s*$", pageCommand.CommandText); + Assert.Contains("COLLATE utf8mb4_bin", pageComposition, StringComparison.OrdinalIgnoreCase); + Assert.Equal("Pomelo.EntityFrameworkCore.MySql", db.Database.ProviderName); + Assert.Empty(commands.Snapshot()); + } + + private static JsonElement[] Items(JsonElement page) => page.GetProperty("items").EnumerateArray().ToArray(); + private static string? Text(JsonElement item, string name) => item.GetProperty(name).ValueKind == JsonValueKind.Null ? null : item.GetProperty(name).GetString(); + + private static CvVariant Variant(int id, string owner, string name, int? jobId = null) => new() + { + Id = id, OwnerUserId = owner, Name = name, JobApplicationId = jobId, PublicSlug = $"documents-{owner}-{id}", + SettingsJson = $"{{\"private\":\"{PrivateContent}\"}}", Version = 2, + CreatedAtUtc = DateTimeOffset.Parse("2026-01-01T00:00:00Z"), UpdatedAtUtc = DateTimeOffset.Parse("2026-01-01T00:00:00Z"), + }; + + private static EmailDraft Draft(Guid id, string owner, int jobId, string subject) => new() + { + Id = id, OwnerUserId = owner, JobApplicationId = jobId, Provider = "gmail", Subject = subject, + To = "private-recipient@example.test", BodyText = PrivateContent, ThreadId = "private-thread-a", ClientRequestId = $"private-client-request-{owner}", + CreatedAtUtc = new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc), UpdatedAtUtc = new DateTime(2026, 1, 4, 0, 0, 0, DateTimeKind.Utc), + }; + + private sealed class Fixture(WebApplication app, string root, CommandRecorder commands) : IAsyncDisposable + { + public CommandRecorder Commands { get; } = commands; + public string DatabaseFile { get; } = Path.Combine(root, "documents.sqlite"); + public string LiveFile { get; } = Path.Combine(root, "private-storage-live.pdf"); + public string ArchivedFile { get; } = Path.Combine(root, "private-storage-archived.pdf"); + public string ArchivedSha256 { get; private set; } = ""; + private readonly HttpClient _client = new() { BaseAddress = new Uri(app.Services.GetRequiredService().Features.Get()!.Addresses.Single()) }; + + public static async Task CreateAsync(bool catalog = false) + { + var root = Path.Combine(Path.GetTempPath(), $"jobjakt-documents-contract-{Guid.NewGuid():N}"); + Directory.CreateDirectory(root); + var commands = new CommandRecorder(); + var builder = WebApplication.CreateBuilder(new WebApplicationOptions { EnvironmentName = "Development", ApplicationName = typeof(AttachmentsController).Assembly.GetName().Name }); + builder.Logging.ClearProviders(); + builder.WebHost.UseKestrel().UseUrls("http://127.0.0.1:0"); + builder.Services.AddControllers().AddApplicationPart(typeof(AttachmentsController).Assembly); + builder.Services.AddAuthentication("local").AddScheme("local", _ => { }); + builder.Services.AddAuthorization(); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddScoped(); + builder.Services.AddDbContext(options => options.UseSqlite($"Data Source={Path.Combine(root, "documents.sqlite")};Pooling=False").AddInterceptors(commands)); + if (typeof(AttachmentsController).Assembly.GetType("JobTrackerApi.Services.DocumentsIndexService") is { } indexService) + builder.Services.AddScoped(indexService); + var users = TestHostFactory.CreateUserManager(); + users.Setup(manager => manager.GetUserAsync(It.IsAny())).ReturnsAsync((ClaimsPrincipal principal) => + LocalAuthIdentity.GetRequiredUserId(principal) is { } owner ? new ApplicationUser { Id = owner } : null); + builder.Services.AddSingleton(users.Object); + var app = builder.Build(); + app.UseAuthentication(); + app.UseAuthorization(); + app.MapControllers(); + await app.StartAsync(); + var fixture = new Fixture(app, root, commands); + await using (var scope = app.Services.CreateAsyncScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); + db.Users.AddRange(new ApplicationUser { Id = OwnerA }, new ApplicationUser { Id = OwnerB }); + db.Companies.AddRange(new Company { Id = 1, OwnerUserId = OwnerA, Name = "Nordlys" }, new Company { Id = 2, OwnerUserId = OwnerB, Name = "Foreign company" }); + db.JobApplications.AddRange(new JobApplication { Id = 1, OwnerUserId = OwnerA, CompanyId = 1, JobTitle = "Owned role", Status = "Saved" }, new JobApplication { Id = 2, OwnerUserId = OwnerB, CompanyId = 2, JobTitle = "Foreign role", Status = "Saved" }); + await db.SaveChangesAsync(); + } + if (catalog) await fixture.SeedCatalogAsync(); + fixture.Commands.Reset(); + return fixture; + } + + public async Task SeedAsync(Action seed) + { + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + using var owner = ((CurrentUserService)scope.ServiceProvider.GetRequiredService()).UseBackgroundUser(OwnerA); + seed(db); + await db.SaveChangesAsync(); + } + + private async Task SeedCatalogAsync() + { + await File.WriteAllBytesAsync(LiveFile, Encoding.UTF8.GetBytes("Live upload ÆØÅ")); + var bytes = Encoding.UTF8.GetBytes("Frozen upload ÆØÅ exact bytes\n"); + await File.WriteAllBytesAsync(ArchivedFile, bytes); + ArchivedSha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var jobs = await db.JobApplications.IgnoreQueryFilters().OrderBy(job => job.Id).ToListAsync(); + foreach (var job in jobs) + { + var own = job.Id == 1; + var owner = own ? OwnerA : OwnerB; + var cvId = own ? 10 : 20; + var cv = Variant(cvId, owner, own ? "Public ÆØÅ CV" : "Foreign CV", job.Id); + cv.IsPublic = own; + if (own) cv.PublicSlug = "documents-public-a"; + db.CvVariants.Add(cv); + job.CoverLetterText = PrivateContent; + job.Notes = $"Human notes\n<<>>\n{PrivateContent}\n<<>>"; + job.TailoredCvText = PrivateContent; + job.TailoredCvUpdatedAt = new DateTime(2026, 1, 5, 0, 0, 0, DateTimeKind.Utc); + db.CoverLetterVersions.AddRange(Enumerable.Range(1, own ? 2 : 1).Select(version => new CoverLetterVersion { OwnerUserId = owner, JobApplicationId = job.Id, Version = version, Text = PrivateContent, CreatedAtUtc = DateTimeOffset.Parse($"2026-01-0{version}T00:00:00Z") })); + db.Attachments.Add(new Attachment { Id = job.Id, JobApplicationId = job.Id, FileName = own ? "Portfolio.pdf" : "Foreign upload.pdf", FilePath = LiveFile, FileSize = 41, FileType = "application/pdf", Purpose = "portfolio", UseForAi = false, UploadDate = new DateTime(2026, 1, 2, 3, 4, 5).AddTicks(1234567) }); + db.SubmittedApplicationPackages.Add(new SubmittedApplicationPackage + { + Id = job.Id, OwnerUserId = owner, JobApplicationId = job.Id, Version = 1, CvVariantId = cvId, + CvVariantName = own ? "Frozen CV ÆØÅ" : "Foreign frozen CV", CvVariantVersion = 1, + CvSettingsJson = PrivateContent, CareerProfileJson = PrivateContent, RenderedCvHtml = PrivateContent, + CoverLetterText = PrivateContent, ApplicationMaterialJson = PrivateContent, CreatedAtUtc = DateTimeOffset.Parse("2026-01-06T00:00:00Z"), + Attachments = [new SubmittedPackageAttachment { FileName = "Archived.pdf", FilePath = ArchivedFile, FileSize = bytes.Length, FileType = "application/pdf", Sha256 = ArchivedSha256 }], + }); + db.EmailDrafts.Add(Draft(Guid.Parse(own ? "11111111-1111-1111-1111-111111111111" : "22222222-2222-2222-2222-222222222222"), owner, job.Id, own ? "Saved subject ÆØÅ" : "Foreign subject")); + } + db.CvVariants.Add(Variant(11, OwnerA, "Independent private CV")); + await db.SaveChangesAsync(); + } + + public async Task GetAsync(string query = "", string owner = OwnerA) + { + using var response = await SendAsync(query, owner); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + return document.RootElement.Clone(); + } + + public async Task StatusAsync(string query, string? owner = OwnerA) + { + using var response = await SendAsync(query, owner); + return response.StatusCode; + } + + private Task SendAsync(string query, string? owner) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"/api/documents{query}"); + if (owner is not null) request.Headers.Add("X-Documents-Test-Owner", owner); + return _client.SendAsync(request); + } + + public async ValueTask DisposeAsync() + { + _client.Dispose(); + await app.StopAsync(); + await app.DisposeAsync(); + Directory.Delete(root, recursive: true); + } + } + + // The fixture authenticates only its two fixed identities; production JWT/session acceptance remains in its existing suite. + private sealed class FixtureAuthentication(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) + { + protected override Task HandleAuthenticateAsync() + { + var owner = Request.Headers["X-Documents-Test-Owner"].ToString(); + if (owner is not (OwnerA or OwnerB)) return Task.FromResult(AuthenticateResult.NoResult()); + var principal = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, owner)], "local")); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, "local"))); + } + } + + private sealed class CommandRecorder : DbCommandInterceptor + { + private readonly List _commands = []; + public void Reset() { lock (_commands) _commands.Clear(); } + public string[] Snapshot() { lock (_commands) return _commands.ToArray(); } + private void Record(DbCommand command) { lock (_commands) _commands.Add(command.CommandText); } + public override InterceptionResult ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { Record(command); return result; } + public override ValueTask> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Record(command); return ValueTask.FromResult(result); } + public override InterceptionResult NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { Record(command); return result; } + public override ValueTask> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Record(command); return ValueTask.FromResult(result); } + public override InterceptionResult ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { Record(command); return result; } + public override ValueTask> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Record(command); return ValueTask.FromResult(result); } + } +} diff --git a/JobTrackerApi/Controllers/DocumentsController.cs b/JobTrackerApi/Controllers/DocumentsController.cs new file mode 100644 index 0000000..1ee6ae5 --- /dev/null +++ b/JobTrackerApi/Controllers/DocumentsController.cs @@ -0,0 +1,36 @@ +using JobTrackerApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace JobTrackerApi.Controllers; + +[ApiController] +[Route("api/documents")] +[Authorize(AuthenticationSchemes = "local")] +public sealed class DocumentsController(DocumentsIndexService documents, ICurrentUserService currentUser) : ControllerBase +{ + [HttpGet] + public async Task> List( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 25, + [FromQuery] string? q = null, + [FromQuery] string type = "all", + [FromQuery] string state = "all", + [FromQuery] string visibility = "all", + [FromQuery] int? jobApplicationId = null, + [FromQuery] string sort = "date-desc", + CancellationToken cancellationToken = default) + { + var owner = currentUser.UserId; + if (owner is null) return Unauthorized(); + if (page is < 1 or > 10000 || pageSize is < 1 or > 100 || q?.Length > 200 || jobApplicationId is <= 0) + return BadRequest("Invalid document pagination or search parameters."); + if (type is not ("all" or "cv" or "cover-letter" or "application-material" or "tailored-cv" or "attachment" or "submitted-package" or "email-draft") + || state is not ("all" or "current" or "draft" or "frozen") + || visibility is not ("all" or "private" or "public") + || sort is not ("date-desc" or "date-asc" or "name-asc" or "name-desc")) + return BadRequest("Unsupported document filter or sort."); + + return Ok(await documents.GetAsync(owner, page, pageSize, q, type, state, visibility, jobApplicationId, sort, cancellationToken)); + } +} diff --git a/JobTrackerApi/Program.cs b/JobTrackerApi/Program.cs index 705008c..4e8c09d 100644 --- a/JobTrackerApi/Program.cs +++ b/JobTrackerApi/Program.cs @@ -84,6 +84,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/JobTrackerApi/Services/DocumentsIndexService.cs b/JobTrackerApi/Services/DocumentsIndexService.cs new file mode 100644 index 0000000..3b7ccaa --- /dev/null +++ b/JobTrackerApi/Services/DocumentsIndexService.cs @@ -0,0 +1,256 @@ +using System.Globalization; +using JobTrackerApi.Data; +using Microsoft.EntityFrameworkCore; + +namespace JobTrackerApi.Services; + +public sealed record DocumentsPageDto(IReadOnlyList Items, int Page, int PageSize, int Total, int TotalPages); + +public sealed record DocumentIndexItemDto( + string Id, + string ResourceId, + string Type, + string Name, + string State, + string? Visibility, + int? JobApplicationId, + string? JobTitle, + string? CompanyName, + bool? JobIsDeleted, + string? RecordedAt, + string DateKind, + long? Version, + int? HistoryCount, + int? AttachmentCount, + int? CvVariantId, + string? CvVariantName, + int? CvVariantVersion, + string? FileType, + long? FileSize, + string? Purpose, + bool? UseForAi, + string? Provider, + string SourceHref, + string? DownloadHref, + string? PublicHref, + bool? HasCurrentContent); + +public sealed class DocumentsIndexService(JobTrackerContext db) +{ + public async Task GetAsync(string owner, int page, int pageSize, string? q, + string type, string state, string visibility, int? jobApplicationId, string sort, CancellationToken ct) + { + var pattern = (q ?? "").Trim().ToLowerInvariant().Replace("!", "!!").Replace("%", "!%").Replace("_", "!_"); + object[] parameters = [owner, $"%{pattern}%", type, state, visibility, jobApplicationId ?? 0, pageSize, (page - 1) * pageSize]; + var sqlite = db.Database.IsSqlite(); + var count = await db.Database.SqlQueryRaw(BuildSql(sqlite, true, sort), parameters).ToListAsync(ct); + var rows = await db.Database.SqlQueryRaw(BuildSql(sqlite, false, sort), parameters).ToListAsync(ct); + var total = count.Single(); + return new DocumentsPageDto(rows.Select(ToDto).ToArray(), page, pageSize, total, (int)Math.Ceiling(total / (double)pageSize)); + } + + private static string BuildSql(bool sqlite, bool count, string sort) + { + const string history = "FROM `CoverLetterVersions` h WHERE h.OwnerUserId = {0} AND h.JobApplicationId = j.Id"; + var letterDate = $"(SELECT CAST(h.CreatedAtUtc AS CHAR) {history} ORDER BY h.Version DESC, h.Id DESC LIMIT 1)"; + var letterCount = $"(SELECT COUNT(*) {history})"; + var letterVersion = $"(SELECT MAX(h.Version) {history})"; + var letterContent = HasContent("j.CoverLetterText"); + var answers = HasAnswers(sqlite); + var recruiter = HasContent("j.RecruiterMessageDraft"); + var tailored = HasContent("j.TailoredCvText"); + var sources = $$""" + SELECT 'cv' AS Type, CAST(v.Id AS CHAR) AS ResourceId, v.Name AS Name, 'current' AS State, + CASE WHEN v.IsPublic = 1 THEN 'public' ELSE 'private' END AS Visibility, + j.Id AS JobApplicationId, j.JobTitle AS JobTitle, c.Name AS CompanyName, j.IsDeleted AS JobIsDeleted, + CAST(v.UpdatedAtUtc AS CHAR) AS RawRecordedAt, 'updated-utc' AS DateKind, + v.Version AS Version, NULL AS HistoryCount, NULL AS AttachmentCount, + NULL AS CvVariantId, NULL AS CvVariantName, NULL AS CvVariantVersion, + NULL AS FileType, NULL AS FileSize, NULL AS Purpose, NULL AS UseForAi, NULL AS Provider, + v.PublicSlug AS PublicSlug, NULL AS HasCurrentContent + FROM `CvVariants` v + LEFT JOIN `JobApplications` j ON j.Id = v.JobApplicationId AND j.OwnerUserId = {0} + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + WHERE v.OwnerUserId = {0} + UNION ALL + SELECT 'cover-letter', CAST(j.Id AS CHAR), j.JobTitle, 'draft', NULL, + j.Id, j.JobTitle, c.Name, j.IsDeleted, {{letterDate}}, + CASE WHEN {{letterDate}} IS NULL THEN 'unknown' ELSE 'history-recorded-utc' END, + {{letterVersion}}, {{letterCount}}, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, CASE WHEN {{letterContent}} THEN 1 ELSE 0 END + FROM `JobApplications` j + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + WHERE j.OwnerUserId = {0} AND ({{letterContent}} OR {{letterCount}} > 0) + UNION ALL + SELECT 'application-material', CAST(j.Id AS CHAR), j.JobTitle, 'draft', NULL, + j.Id, j.JobTitle, c.Name, j.IsDeleted, NULL, 'unknown', + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL + FROM `JobApplications` j + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + WHERE j.OwnerUserId = {0} AND ({{answers}} OR {{recruiter}}) + UNION ALL + SELECT 'tailored-cv', CAST(j.Id AS CHAR), j.JobTitle, 'draft', NULL, + j.Id, j.JobTitle, c.Name, j.IsDeleted, + CAST(COALESCE(d.LastEditedAtUtc, d.LastGeneratedAtUtc, j.TailoredCvUpdatedAt) AS CHAR), + CASE WHEN COALESCE(d.LastEditedAtUtc, d.LastGeneratedAtUtc, j.TailoredCvUpdatedAt) IS NULL THEN 'unknown' ELSE 'updated-utc' END, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL + FROM `JobApplications` j + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + LEFT JOIN `TailoredCvDrafts` d ON d.JobApplicationId = j.Id AND d.OwnerUserId = {0} + WHERE j.OwnerUserId = {0} AND (d.Id IS NOT NULL OR {{tailored}}) + UNION ALL + SELECT 'attachment', CAST(a.Id AS CHAR), a.FileName, 'current', NULL, + j.Id, j.JobTitle, c.Name, j.IsDeleted, CAST(a.UploadDate AS CHAR), 'uploaded-local', + NULL, NULL, NULL, NULL, NULL, NULL, a.FileType, a.FileSize, a.Purpose, a.UseForAi, NULL, NULL, NULL + FROM `Attachments` a + JOIN `JobApplications` j ON j.Id = a.JobApplicationId AND j.OwnerUserId = {0} + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + UNION ALL + SELECT 'submitted-package', CAST(p.Id AS CHAR), COALESCE(p.CvVariantName, j.JobTitle), 'frozen', NULL, + j.Id, j.JobTitle, c.Name, j.IsDeleted, CAST(p.CreatedAtUtc AS CHAR), 'captured-utc', + p.Version, NULL, (SELECT COUNT(*) FROM `SubmittedPackageAttachments` a WHERE a.SubmittedApplicationPackageId = p.Id), + p.CvVariantId, p.CvVariantName, p.CvVariantVersion, NULL, NULL, NULL, NULL, NULL, NULL, NULL + FROM `SubmittedApplicationPackages` p + JOIN `JobApplications` j ON j.Id = p.JobApplicationId AND j.OwnerUserId = {0} + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + WHERE p.OwnerUserId = {0} + UNION ALL + SELECT 'email-draft', CAST(d.Id AS CHAR), d.Subject, 'draft', NULL, + j.Id, j.JobTitle, c.Name, j.IsDeleted, CAST(d.UpdatedAtUtc AS CHAR), 'updated-utc', + d.Revision, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, d.Provider, NULL, NULL + FROM `EmailDrafts` d + JOIN `JobApplications` j ON j.Id = d.JobApplicationId AND j.OwnerUserId = {0} + LEFT JOIN `Companies` c ON c.Id = j.CompanyId AND c.OwnerUserId = {0} + WHERE d.OwnerUserId = {0} + """; + var filtered = $$""" + FROM ({{sources}}) documents + WHERE ({2} = 'all' OR Type = {2}) + AND ({3} = 'all' OR State = {3}) + AND ({4} = 'all' OR (Type = 'cv' AND Visibility = {4})) + AND ({5} = 0 OR JobApplicationId = {5}) + AND ({{Fold("Name")}} LIKE {1} ESCAPE '!' + OR {{Fold("JobTitle")}} LIKE {1} ESCAPE '!' + OR {{Fold("CompanyName")}} LIKE {1} ESCAPE '!' + OR {{Fold("CvVariantName")}} LIKE {1} ESCAPE '!') + """; + if (count) return $"SELECT COUNT(*) AS Value {filtered}"; + var order = sort switch + { + "date-desc" => "(DateSortTicks IS NULL), DateSortTicks DESC", + "date-asc" => "(DateSortTicks IS NULL), DateSortTicks ASC", + "name-asc" => $"{Fold("Name")} ASC", + "name-desc" => $"{Fold("Name")} DESC", + _ => throw new ArgumentOutOfRangeException(nameof(sort)) + }; + return $$""" + SELECT documents.*, {{DateTicks(sqlite)}} AS DateSortTicks {{filtered}} + ORDER BY {{order}}, Type ASC, ResourceId ASC + LIMIT {6} OFFSET {7} + """; + } + + private static string Fold(string value) => $"REPLACE(REPLACE(REPLACE(LOWER(COALESCE({value}, '')), 'Æ', 'æ'), 'Ø', 'ø'), 'Å', 'å')"; + + private static string HasContent(string value) + { + // SQL TRIM does not implement .NET's Unicode whitespace semantics. + var expression = $"COALESCE({value}, '')"; + foreach (var character in "\t\n\v\f\r \u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000") + { + // EF formats SQL line endings; CHAR preserves the exact control whitespace being removed. + var needle = character is >= '\t' and <= '\r' ? $"CHAR({(int)character})" : $"'{character}'"; + expression = $"REPLACE({expression}, {needle}, '')"; + } + return $"LENGTH({expression}) > 0"; + } + + private static string HasAnswers(bool sqlite) + { + string Position(string needle, string value) => sqlite ? $"INSTR({value}, '{needle}')" + : $"LOCATE(CONVERT('{needle}' USING utf8mb4) COLLATE utf8mb4_bin, {value})"; + const string startMarker = "<<>>"; + const string endMarker = "<<>>"; + const string legacyPrefix = "application answer draft:"; + // Keep character offsets aligned while matching modern markers with ordinal case sensitivity. + var notes = sqlite ? "COALESCE(j.Notes, '')" : "CONVERT(COALESCE(j.Notes, '') USING utf8mb4) COLLATE utf8mb4_bin"; + var start = Position(startMarker, notes); + var end = Position(endMarker, notes); + var legacy = Position(legacyPrefix, $"LOWER({notes})" + (sqlite ? "" : " COLLATE utf8mb4_bin")); + return $"CASE WHEN {start} > 0 AND {end} > {start} THEN {HasContent($"SUBSTR({notes}, {start} + {startMarker.Length}, {end} - {start} - {startMarker.Length})")} " + + $"WHEN {legacy} > 0 THEN {HasContent($"SUBSTR({notes}, {legacy} + {legacyPrefix.Length})")} ELSE 0 END"; + } + + private static string DateTicks(bool sqlite) + { + string ticks; + if (sqlite) + { + // Split whole seconds from seven fractional digits; SQLite's fractional date arithmetic rounds. + const string offsetLength = "CASE WHEN SUBSTR(RawRecordedAt, -6, 1) IN ('+', '-') THEN 6 WHEN SUBSTR(RawRecordedAt, -1) = 'Z' THEN 1 ELSE 0 END"; + const string offset = "CASE WHEN SUBSTR(RawRecordedAt, -6, 1) IN ('+', '-') THEN SUBSTR(RawRecordedAt, -6) ELSE '+00:00' END"; + var fraction = $"CASE WHEN SUBSTR(RawRecordedAt, 20, 1) = '.' THEN SUBSTR(RawRecordedAt, 21, LENGTH(RawRecordedAt) - 20 - ({offsetLength})) ELSE '0' END"; + ticks = $"CAST(STRFTIME('%s', SUBSTR(RawRecordedAt, 1, 19) || ({offset})) AS INTEGER) * 10000000 " + + $"+ CAST(SUBSTR(({fraction}) || '0000000', 1, 7) AS INTEGER)"; + } + else ticks = "TIMESTAMPDIFF(MICROSECOND, '1970-01-01 00:00:00', RawRecordedAt) * 10"; + return $"CASE WHEN DateKind IN ('unknown', 'uploaded-local') OR RawRecordedAt IS NULL THEN NULL ELSE {ticks} END"; + } + + private static DocumentIndexItemDto ToDto(DocumentRow row) + { + var resourceId = row.Type == "email-draft" ? Guid.Parse(row.ResourceId).ToString("D") : row.ResourceId; + var source = row.Type switch + { + "cv" => $"/career/builder/{resourceId}", + "attachment" => $"/jobs/{row.JobApplicationId}?section=documents", + "cover-letter" => $"/jobs/{row.JobApplicationId}?section=application&sub=letter", + "application-material" => $"/jobs/{row.JobApplicationId}?section=application&sub=answers", + "tailored-cv" => $"/jobs/{row.JobApplicationId}?section=application&sub=cv", + "submitted-package" => $"/jobs/{row.JobApplicationId}?section=application&sub=submission", + "email-draft" => "/correspondence", + _ => throw new InvalidOperationException("Unsupported document source.") + }; + var recordedAt = row.DateSortTicks.HasValue + ? DateTimeOffset.UnixEpoch.AddTicks(row.DateSortTicks.Value).UtcDateTime.ToString("O", CultureInfo.InvariantCulture) + : row.DateKind == "uploaded-local" && row.RawRecordedAt is not null + ? DateTime.Parse(row.RawRecordedAt, CultureInfo.InvariantCulture).ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff", CultureInfo.InvariantCulture) + : null; + return new DocumentIndexItemDto($"{row.Type}:{resourceId}", resourceId, row.Type, row.Name, row.State, row.Visibility, + row.JobApplicationId, row.JobTitle, row.CompanyName, row.JobIsDeleted, recordedAt, row.DateKind, row.Version, + row.HistoryCount, row.AttachmentCount, row.CvVariantId, row.CvVariantName, row.CvVariantVersion, + row.FileType, row.FileSize, row.Purpose, row.UseForAi, row.Provider, source, + row.Type == "attachment" ? $"/api/attachments/download/{resourceId}" : null, + row.Type == "cv" && row.Visibility == "public" && !string.IsNullOrWhiteSpace(row.PublicSlug) ? $"/cv/{Uri.EscapeDataString(row.PublicSlug)}" : null, + row.HasCurrentContent); + } + + private sealed class DocumentRow + { + public string Type { get; set; } = ""; + public string ResourceId { get; set; } = ""; + public string Name { get; set; } = ""; + public string State { get; set; } = ""; + public string? Visibility { get; set; } + public int? JobApplicationId { get; set; } + public string? JobTitle { get; set; } + public string? CompanyName { get; set; } + public bool? JobIsDeleted { get; set; } + public string? RawRecordedAt { get; set; } + public string DateKind { get; set; } = ""; + public long? Version { get; set; } + public int? HistoryCount { get; set; } + public int? AttachmentCount { get; set; } + public int? CvVariantId { get; set; } + public string? CvVariantName { get; set; } + public int? CvVariantVersion { get; set; } + public string? FileType { get; set; } + public long? FileSize { get; set; } + public string? Purpose { get; set; } + public bool? UseForAi { get; set; } + public string? Provider { get; set; } + public string? PublicSlug { get; set; } + public bool? HasCurrentContent { get; set; } + public long? DateSortTicks { get; set; } + } +} diff --git a/docs/redesign/phase-7/COMMITS.md b/docs/redesign/phase-7/COMMITS.md index 9dbef34..4b2b44e 100644 --- a/docs/redesign/phase-7/COMMITS.md +++ b/docs/redesign/phase-7/COMMITS.md @@ -84,3 +84,11 @@ Inbox/read committed and normal push verified **a791e353b062cc91e10429cd767350ec [Compose report](p7-06/COMPOSE.md) and [portable results](p7-06/evidence/compose-results.json): Compose affected52/5 and fresh backend25 PASS; native10+inbox8+ordinaryP2-D01=19 PASS on LFCrTZzDtX5hNBSRBj5Xx; app/browser TypeScript, lint and build PASS. Actual image review 47/47; all-enabled44scans0violations/330incompletes triaged, human/device/provider qualifications remain. +10front/+0suites,+10browser,+7backend derive739/94,301,1041 only; no integrated full-suite claim. Captured operation/UUID and all field dirty protections, definitive-failure preparation retaining unsaved edits, GET-only recovery and current locale/focus/contrast are verified within recorded bounds. Sole API change marks existing UTC DTO values without tick/data/schema changes. All failed attempts and true source/timing limits remain. Real disconnected server failure precedes provider delivery; simulated delivery states are labelled, actual grants/delivery/manual/production remain unverified. All57groups56PARTIAL/C11DEFERRED and32routes;RD11/RD21/06F/P513 retained. All three mandatoryP7-06 boundaries implemented CONDITIONAL GO; final hygiene/normal push pending. P7-07 contract then UI follows, no Phase 8 ormain push. Final compose hygiene PASS observed2026-10-14 11:01:54.856146UTC: secret/working/staged/phase whitespace gates pass,16 docs/486 local links resolve,57 groups56PARTIAL/C11DEFERRED and32original routes retained. All47finalimages actually reviewed. Implemented scope CONDITIONAL GO; normal development push pending, productionNO-GO/mainunchanged. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. + +### P7-07 metadata contract — final bounded observation + +[Contract report](p7-07/CONTRACT.md) and [portable results](p7-07/evidence/contract-results.json): final12/12 backend contracts (11 nativeSQLiteHTTP, one unexecutedMariaDBcommand/query composition),80/80 existing related/auth/draft cases on matchingReleaseDLL, actualProgram cookie-authenticated3/3 native and ordinaryP2-D01PASS. All26/33/23 captured sources remain unchanged across their runs. BackendRelease compile and held-native browser TypeScript pass; frontendproduct/export remains unchanged. Exact27 metadata fields, two bounded parameterized read queries, owner/parent/company scope, literalNorwegiansearch and knownUTC/local-date distinction preserve existing resources. All failed attempts, sharedWindows-SQLwhitespace repair and parameter-display fixture qualifications remain. No DocumentsUI/axe/manual/MariaDBruntime acceptance. New12backend and3browser derive1053/304; frontend739/94 unchanged, no integrated counts. Backendimplemented scope CONDITIONAL GO; final hygiene/normalpushpending. DocumentsUI follows verified contract push; all57groups56PARTIAL/C11DEFERRED and32routes, mainunchanged/productionNO-GO/noPhase8. + +Final metadata-contract hygiene PASS observed 2026-10-14T16:00:00.842457+00:00: secret and working/staged/phase whitespace gates pass;18 docs/504 local links resolve,57 groups and32 original routes retained. Backend contract implemented CONDITIONAL GO; normal development push pending. Actual MariaDB execution and Documents UI remain unverified; main unchanged, production NO-GO. diff --git a/docs/redesign/phase-7/DEBT.md b/docs/redesign/phase-7/DEBT.md index 1929f7d..5a401bd 100644 --- a/docs/redesign/phase-7/DEBT.md +++ b/docs/redesign/phase-7/DEBT.md @@ -184,3 +184,11 @@ Final inbox hygiene PASS observed2026-10-14 05:14:23UTC: secret and working/stag P7-11 residual localisation evidence from current compose/inbox images: after a saved draft opens in EN, a dirty switch to NB leaves the generic saved-draft context in its opening locale, while controls update. English saved-mail counts use singular `1 attachments` / `1 items`. Actual user text and resource names stay verbatim. These limited copy findings remain open; no blanket localisation acceptance is claimed. Final compose hygiene PASS observed2026-10-14 11:01:54.856146UTC: secret/working/staged/phase whitespace gates pass,16 docs/486 local links resolve,57 groups56PARTIAL/C11DEFERRED and32original routes retained. All47finalimages actually reviewed. Implemented scope CONDITIONAL GO; normal development push pending, productionNO-GO/mainunchanged. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. + +### P7-07 metadata contract — final bounded observation + +[Contract report](p7-07/CONTRACT.md) and [portable results](p7-07/evidence/contract-results.json): final12/12 backend contracts (11 nativeSQLiteHTTP, one unexecutedMariaDBcommand/query composition),80/80 existing related/auth/draft cases on matchingReleaseDLL, actualProgram cookie-authenticated3/3 native and ordinaryP2-D01PASS. All26/33/23 captured sources remain unchanged across their runs. BackendRelease compile and held-native browser TypeScript pass; frontendproduct/export remains unchanged. Exact27 metadata fields, two bounded parameterized read queries, owner/parent/company scope, literalNorwegiansearch and knownUTC/local-date distinction preserve existing resources. All failed attempts, sharedWindows-SQLwhitespace repair and parameter-display fixture qualifications remain. No DocumentsUI/axe/manual/MariaDBruntime acceptance. New12backend and3browser derive1053/304; frontend739/94 unchanged, no integrated counts. Backendimplemented scope CONDITIONAL GO; final hygiene/normalpushpending. DocumentsUI follows verified contract push; all57groups56PARTIAL/C11DEFERRED and32routes, mainunchanged/productionNO-GO/noPhase8. + +Final metadata-contract hygiene PASS observed 2026-10-14T16:00:00.842457+00:00: secret and working/staged/phase whitespace gates pass;18 docs/504 local links resolve,57 groups and32 original routes retained. Backend contract implemented CONDITIONAL GO; normal development push pending. Actual MariaDB execution and Documents UI remain unverified; main unchanged, production NO-GO. diff --git a/docs/redesign/phase-7/PRESERVATION.md b/docs/redesign/phase-7/PRESERVATION.md index 78b6ad4..a55ba73 100644 --- a/docs/redesign/phase-7/PRESERVATION.md +++ b/docs/redesign/phase-7/PRESERVATION.md @@ -194,3 +194,11 @@ Final inbox hygiene PASS observed2026-10-14 05:14:23UTC: secret and working/stag P7-11 residual localisation evidence from current compose/inbox images: after a saved draft opens in EN, a dirty switch to NB leaves the generic saved-draft context in its opening locale, while controls update. English saved-mail counts use singular `1 attachments` / `1 items`. Actual user text and resource names stay verbatim. These limited copy findings remain open; no blanket localisation acceptance is claimed. Final compose hygiene PASS observed2026-10-14 11:01:54.856146UTC: secret/working/staged/phase whitespace gates pass,16 docs/486 local links resolve,57 groups56PARTIAL/C11DEFERRED and32original routes retained. All47finalimages actually reviewed. Implemented scope CONDITIONAL GO; normal development push pending, productionNO-GO/mainunchanged. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. + +### P7-07 metadata contract — final bounded observation + +[Contract report](p7-07/CONTRACT.md) and [portable results](p7-07/evidence/contract-results.json): final12/12 backend contracts (11 nativeSQLiteHTTP, one unexecutedMariaDBcommand/query composition),80/80 existing related/auth/draft cases on matchingReleaseDLL, actualProgram cookie-authenticated3/3 native and ordinaryP2-D01PASS. All26/33/23 captured sources remain unchanged across their runs. BackendRelease compile and held-native browser TypeScript pass; frontendproduct/export remains unchanged. Exact27 metadata fields, two bounded parameterized read queries, owner/parent/company scope, literalNorwegiansearch and knownUTC/local-date distinction preserve existing resources. All failed attempts, sharedWindows-SQLwhitespace repair and parameter-display fixture qualifications remain. No DocumentsUI/axe/manual/MariaDBruntime acceptance. New12backend and3browser derive1053/304; frontend739/94 unchanged, no integrated counts. Backendimplemented scope CONDITIONAL GO; final hygiene/normalpushpending. DocumentsUI follows verified contract push; all57groups56PARTIAL/C11DEFERRED and32routes, mainunchanged/productionNO-GO/noPhase8. + +Final metadata-contract hygiene PASS observed 2026-10-14T16:00:00.842457+00:00: secret and working/staged/phase whitespace gates pass;18 docs/504 local links resolve,57 groups and32 original routes retained. Backend contract implemented CONDITIONAL GO; normal development push pending. Actual MariaDB execution and Documents UI remain unverified; main unchanged, production NO-GO. diff --git a/docs/redesign/phase-7/README.md b/docs/redesign/phase-7/README.md index e81d982..08dcbbf 100644 --- a/docs/redesign/phase-7/README.md +++ b/docs/redesign/phase-7/README.md @@ -14,9 +14,9 @@ The entry commit is `b5319981ad3d1bd635daff861453d23c0de5320b` on `codex/ui-rede | [P7-03](p7-03/README.md) | Career structured editing and import review | Source-ID and structured-editor commits pushed; final import/history runtime/design CONDITIONAL GO: affected76/6, native35, backend98/seeder24, types/lint/build PASS;33/54 final images inspected. Import17 scans0/302, editor13 scans0/377, Career22 scans0/99 retained. Final staged hygiene PASS; normal push5d6e3235 directly verified17:28:54UTC with clean worktree/main unchanged. RD21/human/provider/output gates remain;57/32 unchanged | | [P7-04](p7-04/README.md) | CV library and public entry | Implemented CONDITIONAL GO; library fcacae7 push verified. Public affected38/3/native8/types/lint/build and fresh fidelity16/36+8/24 PASS;18 images actually reviewed, frame-tested12/manual gates retained. Public final hygiene PASS/normal push bfa0ab6 directly verified22:24:44UTC, clean main unchanged | | [P7-05](p7-05/README.md) | Discovery, Companies, Offers and Reminders | Entry bfa0ab6 verified; Discovery21/1,new8+existing4native/backend14/types/lint/build pass;18 actual PNG reviews/qualified81+52incompletes. Discovery implemented CONDITIONAL GO, hygiene PASS/normal push34a4d6d verified; Companies30/4,new6+old2+named4native/backend2/types/lint/build pass;20actual PNG reviews/qualified91+163incompletes. Companies implemented CONDITIONAL GO, hygiene PASS/normal pusha8aba85 verified; Offers9/1,new4native/backend6/types/lint/build pass;14actual PNG reviews/15incompletes qualified. Offers implemented CONDITIONAL GO, hygiene PASS/normal pushc723a4e verified; Reminders17/3,new6native/backend6/types/lint/build pass;17actual PNG reviews/114incompletes qualified. P7-05 implemented CONDITIONAL GO, hygiene PASS/normal push627955a directly verified11:49:32UTC, clean localHEAD=origin=live/main unchanged; P7-06 next | -| [P7-06](p7-06/README.md) | Remaining Application forms and global correspondence | All three boundaries implemented CONDITIONAL GO: forms4c325/inboxa791 normally pushed; [compose](p7-06/COMPOSE.md) Compose affected52/5 and fresh backend25 PASS; native10+inbox8+ordinaryP2-D01=19 PASS on LFCrTZzDtX5hNBSRBj5Xx; app/browser TypeScript, lint and build PASS. Actual image review 47/47; all-enabled44scans0violations/330incompletes triaged, human/device/provider qualifications remain. +10front/+0suites,+10browser,+7backend derive739/94,301,1041 only; no integrated full-suite claim. Final hygiene/normal push pending;06Fdeferred/57groups32routes/main unchanged; P7-07 contract first next | +| [P7-06](p7-06/README.md) | Remaining Application forms and global correspondence | All three boundaries implemented CONDITIONAL GO: forms4c325/inboxa791 normally pushed; [compose](p7-06/COMPOSE.md) Compose affected52/5 and fresh backend25 PASS; native10+inbox8+ordinaryP2-D01=19 PASS on LFCrTZzDtX5hNBSRBj5Xx; app/browser TypeScript, lint and build PASS. Actual image review 47/47; all-enabled44scans0violations/330incompletes triaged, human/device/provider qualifications remain. +10front/+0suites,+10browser,+7backend derive739/94,301,1041 only; no integrated full-suite claim. Hygiene PASS and normal223dcf5 push directly verified11:03:15UTC;06Fdeferred/57groups32routes/main unchanged; P7-07 contract first next | | P7-06F | Deferred `followMode` reviewed composer | Optional separately reviewed mail slice; **DEFERRED** until mode→draft/review action and fallback are explicitly decided | -| P7-07 | Documents owner-scoped index | Authenticated metadata-only paginated backend projection first; source destinations from P7-04/06; planned | +| [P7-07](p7-07/README.md) | Documents owner-scoped index | Backendcontract implemented CONDITIONAL GO:12/12 new contracts,80/80 related/auth/draft,actualcookie native3+ordinaryP2-D01PASS;26/33/23sourcesheld. Finalhygiene/normalpushpending; actual MariaDB runtime NOT VERIFIED. UIwaitscontractpush; no app route added yet | | P7-08 | Existing notifications and operations | Existing store and real destinations; new enabled producers require F20 recovery evidence; planned | | P7-09 | Remaining Settings, Connections and data lifecycle | Existing independent save scopes and sensitive-action guards; planned | | P7-10 | Admin operational presentation | Existing distinct Users/Audit/System routes and server authorization; planned | @@ -72,3 +72,11 @@ Inbox/read committed and normal push verified **a791e353b062cc91e10429cd767350ec [Compose report](p7-06/COMPOSE.md) and [portable results](p7-06/evidence/compose-results.json): Compose affected52/5 and fresh backend25 PASS; native10+inbox8+ordinaryP2-D01=19 PASS on LFCrTZzDtX5hNBSRBj5Xx; app/browser TypeScript, lint and build PASS. Actual image review 47/47; all-enabled44scans0violations/330incompletes triaged, human/device/provider qualifications remain. +10front/+0suites,+10browser,+7backend derive739/94,301,1041 only; no integrated full-suite claim. Captured operation/UUID and all field dirty protections, definitive-failure preparation retaining unsaved edits, GET-only recovery and current locale/focus/contrast are verified within recorded bounds. Sole API change marks existing UTC DTO values without tick/data/schema changes. All failed attempts and true source/timing limits remain. Real disconnected server failure precedes provider delivery; simulated delivery states are labelled, actual grants/delivery/manual/production remain unverified. All57groups56PARTIAL/C11DEFERRED and32routes;RD11/RD21/06F/P513 retained. All three mandatoryP7-06 boundaries implemented CONDITIONAL GO; final hygiene/normal push pending. P7-07 contract then UI follows, no Phase 8 ormain push. Final compose hygiene PASS observed2026-10-14 11:01:54.856146UTC: secret/working/staged/phase whitespace gates pass,16 docs/486 local links resolve,57 groups56PARTIAL/C11DEFERRED and32original routes retained. All47finalimages actually reviewed. Implemented scope CONDITIONAL GO; normal development push pending, productionNO-GO/mainunchanged. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. + +### P7-07 metadata contract — final bounded observation + +[Contract report](p7-07/CONTRACT.md) and [portable results](p7-07/evidence/contract-results.json): final12/12 backend contracts (11 nativeSQLiteHTTP, one unexecutedMariaDBcommand/query composition),80/80 existing related/auth/draft cases on matchingReleaseDLL, actualProgram cookie-authenticated3/3 native and ordinaryP2-D01PASS. All26/33/23 captured sources remain unchanged across their runs. BackendRelease compile and held-native browser TypeScript pass; frontendproduct/export remains unchanged. Exact27 metadata fields, two bounded parameterized read queries, owner/parent/company scope, literalNorwegiansearch and knownUTC/local-date distinction preserve existing resources. All failed attempts, sharedWindows-SQLwhitespace repair and parameter-display fixture qualifications remain. No DocumentsUI/axe/manual/MariaDBruntime acceptance. New12backend and3browser derive1053/304; frontend739/94 unchanged, no integrated counts. Backendimplemented scope CONDITIONAL GO; final hygiene/normalpushpending. DocumentsUI follows verified contract push; all57groups56PARTIAL/C11DEFERRED and32routes, mainunchanged/productionNO-GO/noPhase8. + +Final metadata-contract hygiene PASS observed 2026-10-14T16:00:00.842457+00:00: secret and working/staged/phase whitespace gates pass;18 docs/504 local links resolve,57 groups and32 original routes retained. Backend contract implemented CONDITIONAL GO; normal development push pending. Actual MariaDB execution and Documents UI remain unverified; main unchanged, production NO-GO. diff --git a/docs/redesign/phase-7/p7-06/COMPOSE.md b/docs/redesign/phase-7/p7-06/COMPOSE.md index bdec831..e78989b 100644 --- a/docs/redesign/phase-7/p7-06/COMPOSE.md +++ b/docs/redesign/phase-7/p7-06/COMPOSE.md @@ -1,6 +1,6 @@ # P7-06 — Compose and send -**Implemented scope: CONDITIONAL GO. Affected regression, final native consumers, actual47-image review and finite hygiene pass; normal development push pending.** This is the third mandatory P7-06 boundary after [embedded forms](FORMS.md) and [inbox/read review](INBOX.md). Follow the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md#migration-order-and-commit-boundaries); optional P7-06F remains DEFERRED pending its mode→draft/review action and fallback decision. +**Implemented scope: CONDITIONAL GO. Affected regression, final native consumers, actual47-image review and finite hygiene pass; normal development push223dcf5 verified.** This is the third mandatory P7-06 boundary after [embedded forms](FORMS.md) and [inbox/read review](INBOX.md). Follow the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md#migration-order-and-commit-boundaries); optional P7-06F remains DEFERRED pending its mode→draft/review action and fallback decision. [Compose entry](evidence/compose-entry.json) records the preceding inbox commit `a791e353b062cc91e10429cd767350ec9f0aaef2`. Its normal push was observed on **2026-10-14 at 05:14:53.828571 UTC**: clean local HEAD = origin = live `codex/ui-redesign-phase4`; local/origin/live `main` remained `27657f31131cd95d42f0e1290853ec9c9b3959a4`. That receipt establishes synchronization of the predecessor, not acceptance or push of this slice. @@ -110,3 +110,5 @@ Complete visual review, run finite documentation/secret/whitespace/route/preserv All **47 final PNGs** were actually displayed and reviewed: root16 and independent reviewer31, with exact per-image references in the portable result. No blocking visual defect was found. Full-page/sticky/dialog/transition captures and resized long images retain their stated limits. In addition to the opening-locale draft-context label, English singular count copy (`1 attachments`, `1 items`) remains a P7-11 residual. No human reader, physical-device, zoom, provider or production acceptance follows. Final runtime/source/visual gates establish bounded **CONDITIONAL GO**; finite hygiene and normal development push follow below. Final compose hygiene PASS observed2026-10-14 11:01:54.856146UTC: secret/working/staged/phase whitespace gates pass,16 docs/486 local links resolve,57 groups56PARTIAL/C11DEFERRED and32original routes retained. All47finalimages actually reviewed. Implemented scope CONDITIONAL GO; normal development push pending, productionNO-GO/mainunchanged. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. diff --git a/docs/redesign/phase-7/p7-06/README.md b/docs/redesign/phase-7/p7-06/README.md index d19353b..f062ddf 100644 --- a/docs/redesign/phase-7/p7-06/README.md +++ b/docs/redesign/phase-7/p7-06/README.md @@ -1,6 +1,6 @@ # P7-06 — Remaining Application forms and global correspondence -**Implemented scope: CONDITIONAL GO.** All three approved boundaries are implemented. Embedded forms and inbox/read are normally pushed; compose/send has final runtime evidence, with hygiene and normal push pending below. Production remains NO-GO; main unchanged. P7-06F stays DEFERRED. +**Implemented scope: CONDITIONAL GO.** All three approved boundaries are implemented. All three implementation commits are normally pushed and verified, including compose223dcf5. Manual/provider qualifications remain below. Production remains NO-GO; main unchanged. P7-06F stays DEFERRED. The exact [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md#migration-order-and-commit-boundaries) governs the sequence: embedded forms → inbox/read review → compose/send. No generic Documents persistence, new provider capability, withheld AI review or Phase8 feature was introduced. @@ -33,3 +33,5 @@ M01–M12, provider grants/delivery, physical touch/software keyboard, human rea Next approved checkpoint is **P7-07**: metadata-only authenticated paginated contract commit first, then Documents UI. Do not start Phase8 or deploy. Final compose hygiene PASS observed2026-10-14 11:01:54.856146UTC: secret/working/staged/phase whitespace gates pass,16 docs/486 local links resolve,57 groups56PARTIAL/C11DEFERRED and32original routes retained. All47finalimages actually reviewed. Implemented scope CONDITIONAL GO; normal development push pending, productionNO-GO/mainunchanged. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. diff --git a/docs/redesign/phase-7/p7-06/evidence/compose-results.json b/docs/redesign/phase-7/p7-06/evidence/compose-results.json index 3b3de09..d7ed6ad 100644 --- a/docs/redesign/phase-7/p7-06/evidence/compose-results.json +++ b/docs/redesign/phase-7/p7-06/evidence/compose-results.json @@ -3,7 +3,7 @@ "checkpoint": "P7-06", "slice": "Compose/send", "recordedUtc": "2026-10-14T11:01:30.155858+00:00", - "acceptance": "COMPOSE_IMPLEMENTED_SCOPE_CONDITIONAL_GO_HYGIENE_PASS_NORMAL_PUSH_PENDING", + "acceptance": "COMPOSE_IMPLEMENTED_SCOPE_CONDITIONAL_GO_HYGIENE_PASS_NORMAL_PUSH_VERIFIED", "precedingInboxCommit": "a791e353b062cc91e10429cd767350ec9f0aaef2", "immutableFinalCapsule": { "path": ".artifacts/phase7/p7-06/compose/native-final-verified/results.json", @@ -3219,5 +3219,106 @@ "Local links check target existence, not URL/heading anchors.", "Static evidence is not runtime/manual/device/provider/production acceptance." ] + }, + "commitHash": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "normalPushVerification": { + "observedUtc": "2026-10-14T11:03:15.576853+00:00", + "branch": "codex/ui-redesign-phase4", + "head": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "originDevelopment": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "liveDevelopment": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "liveMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "workingTreeClean": true, + "pendingPaths": [], + "commands": [ + { + "command": [ + "rtk", + "proxy", + "git", + "branch", + "--show-current" + ], + "exitCode": 0, + "stdout": "codex/ui-redesign-phase4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "HEAD" + ], + "exitCode": 0, + "stdout": "223dcf57d26bab624261994b61a46f2d3e2dd5b2\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "origin/codex/ui-redesign-phase4" + ], + "exitCode": 0, + "stdout": "223dcf57d26bab624261994b61a46f2d3e2dd5b2\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "main" + ], + "exitCode": 0, + "stdout": "27657f31131cd95d42f0e1290853ec9c9b3959a4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "origin/main" + ], + "exitCode": 0, + "stdout": "27657f31131cd95d42f0e1290853ec9c9b3959a4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "ls-remote", + "origin", + "refs/heads/codex/ui-redesign-phase4", + "refs/heads/main" + ], + "exitCode": 0, + "stdout": "223dcf57d26bab624261994b61a46f2d3e2dd5b2\trefs/heads/codex/ui-redesign-phase4\n27657f31131cd95d42f0e1290853ec9c9b3959a4\trefs/heads/main\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "status", + "--porcelain=v1" + ], + "exitCode": 0, + "stdout": "", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + } + ] } } diff --git a/docs/redesign/phase-7/p7-07/CONTRACT.md b/docs/redesign/phase-7/p7-07/CONTRACT.md new file mode 100644 index 0000000..b4c9fac --- /dev/null +++ b/docs/redesign/phase-7/p7-07/CONTRACT.md @@ -0,0 +1,85 @@ +# P7-07 — Documents metadata contract + +**Status: backend contract implemented, implemented-scope CONDITIONAL GO; final affected/native checks pass. Hygiene, commit and normal push remain pending. P7-07 remains ongoing; Documents UI waits for the contract push.** This is the first P7-07 boundary in the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md#migration-order-and-commit-boundaries), implementing the owner projection required by [Documents architecture](../../phase-3/DOCUMENTS_UX.md). + +## Bounded implementation + +`DocumentsController`, `DocumentsIndexService` and its scoped Program registration add authenticated **GET `/api/documents`** over existing resources. No generic Document table, schema migration, storage relocation, browser crawl, editor replacement, PDF generation or mail-persistence change is introduced. + +The response envelope contains exactly `items`, `page`, `pageSize`, `total` and `totalPages`. Each item has exactly **27 public metadata fields**: + +`id`, `resourceId`, `type`, `name`, `state`, `visibility`, `jobApplicationId`, `jobTitle`, `companyName`, `jobIsDeleted`, `recordedAt`, `dateKind`, `version`, `historyCount`, `attachmentCount`, `cvVariantId`, `cvVariantName`, `cvVariantVersion`, `fileType`, `fileSize`, `purpose`, `useForAi`, `provider`, `sourceHref`, `downloadHref`, `publicHref`, `hasCurrentContent`. + +| Existing kind | Honest projection and destination | +|---|---| +| `cv` | Current variant name/version, optional owned job, private/public state; existing builder and active public slug. No GET PDF action is invented | +| `cover-letter` | Job draft and recorded history, including history after current text is cleared; `hasCurrentContent` distinguishes those states. Latest history date is not a claim of current-edit time; open Application → Letter | +| `application-material` | Existing saved answer/recruiter-draft presence; unknown date stays unknown. Ordinary Notes do not become a document; open Application → Answers | +| `tailored-cv` | Existing structured draft or legacy tailored text, with recorded edit/generation/update date when available; open Application → CV, without inventing a downloadable file | +| `attachment` | Current upload metadata, purpose/type/size/date and existing explicit AI-use flag; existing job Documents and authenticated download | +| `submitted-package` | Frozen package version/capture time, captured CV identifiers/name/version and captured attachment count; existing Submission context. Current CV/upload changes cannot relabel captured metadata | +| `email-draft` | Saved subject/provider/revision and owned job context; draft does not mean delivery. Open Correspondence; P7-06 remains the persistence/send owner | + +`sourceHref` opens the existing owner context. Correspondence does not promise exact-ID Resume, and Submission does not promise automatic selection of a particular package. Attachment `downloadHref` is `/api/attachments/download/{id}`: the UI must use the existing authenticated download path without duplicating an Axios `/api` base. CV download remains the existing explicit export operation; list GET cannot render it. + +## Filtering, ownership and database bounds + +| Parameter | Supported contract | +|---|---| +| `page` / `pageSize` | Defaults 1/25; ranges 1–10000 and 1–100 | +| `q` | At most 200 characters; trimmed metadata search with literal `%`, `_` and `!`, plus supported Nordic case folding | +| `type` | `all` or one of the seven kinds above | +| `state` | `all`, `current`, `draft`, `frozen` | +| `visibility` | `all`, `private`, `public`; specific visibility intersects CV entries only | +| `jobApplicationId` | Optional positive owned-parent filter; a foreign or missing job returns no private metadata | +| `sort` | `date-desc`, `date-asc`, `name-asc`, `name-desc`; deterministic Type/ResourceId ordinal ties | + +The local authentication scheme and server current-user service choose the owner. Caller-supplied owner IDs cannot select another tenant. Every UNION branch guards its owner; parent/company joins also guard ownership. Malformed foreign associations expose no foreign context. Independent/detached CVs remain available to their owner; detach does not delete a variant. Owned soft-deleted job history remains visible with `jobIsDeleted`, rather than silently applying an active-Jobs-only policy. + +The service executes **two parameterized SELECT commands**: filtered count and a database `LIMIT`/`OFFSET` page. It does not materialize the complete collection to sort in memory. A 128-item SQLite fixture proves a seven-row second page, exact count, two read commands and unchanged database bytes. Invalid parameters fail before catalog queries. Search covers names/role/company/captured CV name, never private content snippets. SQL computes existing text presence without projecting bodies, Notes, settings/profile JSON, rendered HTML, storage paths, recipient/thread/request identities or bytes to the response. No renderer, AI, file-copy or listing-write dependency is registered in the contract fixture. + +SQLite ordering separates whole UTC seconds from seven fractional digits to preserve exact instants, offsets and one-tick differences. Unknown dates and uploads whose original local timezone was not recorded sort last in both date directions. Uploads retain their raw unzoned wall-clock value as `uploaded-local`; no UTC instant is fabricated. MariaDB SQL composes `TIMESTAMPDIFF(MICROSECOND, …) * 10`; live MariaDB execution and production precision remain unverified. + +## Reproduction and retained failures + +All times are UTC on **2026-10-14**. Exact commands/TRX/source receipts remain under `.artifacts/phase7/current/backend-career/` and `.artifacts/phase7/p7-07/`; failed evidence is retained. + +| Observation | Actual result and boundary | +|---|---| +| Existing related baseline | **37 PASS**, 11:05:19.519871–11:05:23.691550, `--no-build`; 20 pre/post sources and inherited API DLL match. Newly written Documents tests were not compiled in this lane | +| Initial controller HTTP RED | **0 pass/11 fail**, 11:12:34.372367–11:12:50.335904; missing endpoint returns 404. Eleven ordinary Kestrel/SQLite tests use controlled test identities, not actual Program cookies | +| Actual Program old-API RED | **0 pass/3 fail**, 11:25:16.963090–11:25:28.045792; actual local Identity sessions/CSRF-protected source writes encounter missing `/documents` 404 | +| MariaDB policy RED | **0 pass/1 fail**, 11:27:50.588566–11:28:02.188395; composition lacks the explicit ordinal Unicode marker policy. No MariaDB connection or execution occurs | +| First full candidate | **10 pass/2 fail**, 11:30:29.637426–11:30:41.875848; marker-presence and pagination-composition checks remain unresolved | +| Focused diagnostic | **0 pass/2 fail**, 15:41:17.837210–15:41:31.709973; exact empty-LF `application-material:5`/whitespace `:7` membership and literal debug-display LIMIT fail. No earlier ten-pass rerun is inferred | +| Corrected full candidate | **12 PASS**, 15:47:19.771612–15:47:36.190838; 26 pre/post sources match. TRX SHA256 `cf5791cef3e159d144cb5a4b0628beeba78562d1975676108458005e028e2277` | +| Final strengthened fixture | **12 PASS**, 15:50:24.880378–15:50:35.341407; all BMP `char.IsWhiteSpace` characters exercise the same presence rule. All 26 pre/post sources match; zero failed/not-executed cases. TRX SHA256 `4d34e2045ced1a0b2bd3251893a5cad0e012c396fabd8b989466a84b2365b139` | +| Final related/authentication lanes | **80 PASS**, 15:52:48.930990–15:52:53.240041, `--no-build`: 37 existing source/archive contracts, 25 draft/send cases and 18 authentication posture/session cases. All 33 pre/post sources match; same final Release DLL | +| Actual Program current-API native | **3 PASS**, 15:54:17.479545–15:54:29.733344; actual local Identity cookies, disposable SQLite, independent A/B owners and existing CSRF-protected source mutations | +| Ordinary P2-D01 consumer | **1 PASS**, 15:54:29.738467–15:54:43.075754; existing frozen-document workspace case, without skip or expected-failure marker | + +The old-API receipt named `pre.json` was actually captured **11:26:04.698648**, after the native command ended **11:25:28.045792**. Its raw “DURING” wording is inaccurate; `native-red-old-api/capture-qualification.json` preserves and corrects it. That receipt is an after-run observation, not pre/during proof. The separately frozen prior compile/source/binary boundary qualifies the old-API comparison. A usage pause precedes the 15:40 resumption and held-file verification; no tests or outcomes are inferred during that gap. + +Modern answer markers remain ordinal and case-sensitive, including text with Unicode before them. MariaDB `CONVERT(… USING utf8mb4) COLLATE utf8mb4_bin` preserves character positions; legacy prefix matching remains case-insensitive and empty modern content retains precedence. Shared `HasContent` removes .NET whitespace. `CHAR(9)` through `CHAR(13)` preserve control characters despite EF/Windows SQL newline formatting; SQL TRIM alone was insufficient. Assertions retain the exact four expected document IDs and no private-content leakage. + +Pomelo `ToQueryString()` displays literal pagination values. The corrected test separately inspects an **actual, unexecuted DbCommand**, proving distinct bound LIMIT/OFFSET parameters with expected values and owner binding. This is opaque SQL composition evidence: **11 SQLite HTTP cases + one MariaDB composition case**, not twelve provider executions or a TCP/server qualification. + +Final service SHA256 is `def370bd0dbe90260e42800fded99302e56f8ac6b7693e73dd0a60cf0a3f7fd1`; strengthened test SHA256 is `f16d3d78b072faaea87c376888803ecbf8daec6da62368d89ea78ecd8bc9bac2`. `contract-final` retains genuine pre/post captures and the API DLL SHA256 `5bc23b88df160434f15b5cd09535144bdac3eeebc241d5e1837d854d936f4842`. + +## Remaining acceptance and preservation + +The three passing API-only native cases cover actual cookie owner isolation, exact metadata/filter/page/search contracts and observable source revision/detach/delete readback. They do not render a Documents page, exercise downloads or prove internal SQL/service-call counts. The backend fixture separately proves all seven kinds, read bounds and unchanged captured metadata/archived bytes after live CV/upload/letter changes. + +Final native capsule `.artifacts/phase7/p7-07/native-final/report/results.json` SHA256 is `baa1d592e5408220ed9d6f9742af907014c275cbbcb3ad76fb7117435ace345e`. All 23 product/specification pre/post inputs and recorded binaries remain unchanged across native three cases and P2-D01. Browser TypeScript passed at 11:25 on unchanged specification SHA256 `aca359ec044332ed688bd108352c28d6f4c960c8201dd4a7853b6281d341f483`. The inherited frontend export `LFCrTZzDtX5hNBSRBj5Xx` is an unused UI launch fixture for the API-only cases; P2-D01 uses the existing workspace. No new frontend build or Jest execution is claimed. + +Root actually inspected the sole P2-D01 consumer PNG. Its opacity transition and full-page sticky-element capture limit visual conclusions; it is not Documents UI evidence or a fresh rendering-fidelity qualification. API-only cases perform no axe scans. Independent source review found no critical issue; that review supplies no additional runtime, provider or MariaDB evidence. + +Release contract execution, related/authentication cases, native cases, P2-D01 and browser TypeScript are recorded above. Secret/documentation/whitespace checks, commit and normal development push remain pending. After the contract push is verified, implement the separate Documents UI boundary using these honest states and existing owner actions. + +The twelve additions derive backend inventory **1,053** from **1,041**, not a fresh full-suite pass count. Three new browser cases derive **304** from **301**; publication remains pending. Frontend inventory stays **739/94**, with no Jest changes. P7-12 owns fresh integrated counts; inherited full backend 1,033/frontend 573/89/sidecar 49 are not replaced by these focused runs. + +All **57 PRES groups remain 56 PARTIAL / C11 DEFERRED**; C05–C12/W05–W08/M04/S05/E09 receive bounded contract evidence, not whole-group PASS. Preserve the original routes, canonical Career Profile authority, CV overrides/associations, frozen content and captured files. P5-13 stays withheld with zero qualification calls; optional followMode stays deferred. No manual UI/visual, reader/device, provider or production acceptance is claimed. + +**Contract implemented-scope acceptance: CONDITIONAL GO; hygiene and normal push pending. P7-07: ongoing; Documents UI not started. Actual MariaDB execution and manual acceptance remain unverified. Production: NO-GO; main unchanged. Phase 8: no implementation.** + +Final metadata-contract hygiene PASS observed 2026-10-14T16:00:00.842457+00:00: secret and working/staged/phase whitespace gates pass;18 docs/504 local links resolve,57 groups and32 original routes retained. Backend contract implemented CONDITIONAL GO; normal development push pending. Actual MariaDB execution and Documents UI remain unverified; main unchanged, production NO-GO. diff --git a/docs/redesign/phase-7/p7-07/README.md b/docs/redesign/phase-7/p7-07/README.md new file mode 100644 index 0000000..45775ba --- /dev/null +++ b/docs/redesign/phase-7/p7-07/README.md @@ -0,0 +1,31 @@ +# P7-07 — Documents owner-scoped index + +**Backend implemented scope: CONDITIONAL GO; final hygiene and normal push pending. Documents UI has not started.** Follow the exact [Phase 6 handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md#migration-order-and-commit-boundaries) and [Documents architecture](../../phase-3/DOCUMENTS_UX.md). Implement and push the metadata projection before the interface. + +The preceding compose commit `223dcf57d26bab624261994b61a46f2d3e2dd5b2` was normally pushed and directly verified at **2026-10-14 11:03:15.576853 UTC**: clean HEAD = origin = live development; local/origin/live main remains `27657f31131cd95d42f0e1290853ec9c9b3959a4`. [Entry evidence](evidence/entry.json) records the receipt and fresh existing-contract baseline: **37 passing cases**, unchanged Release DLL, with all20 captured sources unchanged before/after. This is an existing-contract subset, not a Documents endpoint or full-suite execution. + +## Bounded contract + +Authenticated `GET /api/documents` indexes existing CV variants, cover letters/history, application-answer/recruiter drafts, tailored CV drafts, attachments, frozen submitted packages and saved email drafts. It returns metadata, recorded source identifiers and existing destinations. It does not return bodies, notes, settings/profile JSON, rendered HTML, storage paths, bytes, recipient addresses, thread IDs or sending UUIDs. + +Use validated pagination, literal metadata search, supported type/state/public-visibility/job filters and whitelisted name/date sorts. Count and page must execute in the database with bounded results. Every source, parent and company join must enforce the authenticated owner. Preserve independent/detached CVs and existing owned resources attached to soft-deleted jobs; expose the deleted-parent flag honestly. + +Current CV/attachment, unsent draft material and frozen package are different states. CV public/private is independent of submission. A cover-letter history entry survives clearing current text and records whether current content exists. Submitted metadata uses captured CV identifiers/name/version and attachment counts; it cannot resolve these from mutable current sources. Uploading a certificate does not verify a qualification. A draft does not prove delivery; a captured package does not prove employer receipt. + +Date sorting compares genuinely recorded UTC instants, including exact offset/fractional ordering. Legacy attachment `UploadDate` uses `DateTime.Now` without recording its timezone. Preserve its recorded local date for display and classify it `uploaded-local`; its UTC sort key is unavailable. Local/unknown dates remain after comparable dates in both ascending and descending order, with stable type/resource identity ties. Do not label this mixed index “last updated” or infer a timezone. Name sorting is unaffected. This policy requires explicit UI explanation and regression coverage. + +## Validation and preservation + +New ordinary HTTP regressions must fail against the missing endpoint before implementation, then pass with native SQLite. Cover every source, A/B/anonymous isolation, search/filter/count/pagination, literal wildcard characters, stable ties, offset/fractional dates, unknown-date placement, malformed relationships and source-history preservation. Inspect commands for count/order/limit/offset and absence of resource-body projection or writes. Listing must never render PDF, invoke AI, capture a package or access file storage. + +Keep ordinary P2-D01 and exact frozen HTML/letter/archive-byte proof. The old Phase3 P2-D01 blocker is historical: the latest ordinary browser case passes in [P7-06](../p7-06/COMPOSE.md). Source actions retain existing ownership, CV association/detach rules, revision semantics and safe downloads. Native SQLite and MariaDB SQL composition are separate evidence; unexecuted MariaDB behavior cannot become production acceptance. + +All57 preservation groups remain56 PARTIAL / C11 DEFERRED, with32 original routes preserved. Derived inventory is739/94 frontend,301 browser,1041 backend; it is not a fresh integrated pass count. Human reader, physical touch/software keyboard, zoom, provider/output and release conditions remain open. P5-13 is withheld; P7-06F is deferred; main unchanged, production NO-GO. No Phase8 implementation. + +Next: finish the backend contract and its bounded evidence, commit/push/verify it, then migrate the Documents interface using the existing shared design system. + +## Final backend observation + +[Contract report](CONTRACT.md) and [portable results](evidence/contract-results.json) record final12/12 new contracts,80/80 related/auth/draft cases and actualProgram/nativeSQLite cookie3/3 plus ordinaryP2-D01PASS. Full .NET whitespace semantics, ordinal Unicode marker handling, actual bound pagination parameters, frozen metadata/bytes and strict tenant cases retain ordinary assertions. All failed attempts and source/timing qualifications remain. Final26/33/23 sources and binaries match; Release compilation and browser types pass. There is no DocumentsUI/axe/physical/human acceptance yet, and actualMariaDB execution remains unverified. Derivedbackend1053/browser304/frontend739/94 are not integrated passing counts. Hygiene and normal development push follow; the UI starts only after verified contract push. + +Final metadata-contract hygiene PASS observed 2026-10-14T16:00:00.842457+00:00: secret and working/staged/phase whitespace gates pass;18 docs/504 local links resolve,57 groups and32 original routes retained. Backend contract implemented CONDITIONAL GO; normal development push pending. Actual MariaDB execution and Documents UI remain unverified; main unchanged, production NO-GO. diff --git a/docs/redesign/phase-7/p7-07/evidence/contract-results.json b/docs/redesign/phase-7/p7-07/evidence/contract-results.json new file mode 100644 index 0000000..46359b3 --- /dev/null +++ b/docs/redesign/phase-7/p7-07/evidence/contract-results.json @@ -0,0 +1,1310 @@ +{ + "schemaVersion": 1, + "checkpoint": "P7-07", + "slice": "Backend metadata contract", + "recordedUtc": "2026-10-14T15:59:01.541142+00:00", + "acceptance": "BACKEND_CONTRACT_IMPLEMENTED_SCOPE_CONDITIONAL_GO_HYGIENE_PASS_NORMAL_PUSH_PENDING", + "entry": { + "path": "docs/redesign/phase-7/p7-07/evidence/entry.json", + "bytes": 7037, + "sha256": "749704f885b31b9080853fb55dd11c31e34615cf8ac5a7c68c6761d848573db4", + "hashPolicy": "Exact raw bytes" + }, + "precedingComposeCommit": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "finalBackend": { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-restore", + "--filter", + "FullyQualifiedName~DocumentsControllerTests", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-documents-final-20261014T155024Z" + ], + "startedUtc": "2026-10-14T15:50:24.880378+00:00", + "finishedUtc": "2026-10-14T15:50:35.341407+00:00", + "exitCode": 0, + "logSha256": "d7de6b459d2ccc7d46d260a3bf313512a5b5fc77e6c7d8a8aae0b8056c3dc4d8", + "counts": { + "total": 12, + "passed": 12, + "failed": 0, + "notExecuted": 0 + }, + "resultSha256": "4d34e2045ced1a0b2bd3251893a5cad0e012c396fabd8b989466a84b2365b139" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-final-20261014T155024Z/command.json", + "bytes": 902, + "sha256": "257412e0ddb1bfc3203fd75ba9cbca9359b7ea65d46be68ca72841de79cb9ed7", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-final-20261014T155024Z/career.log", + "bytes": 988, + "sha256": "d7de6b459d2ccc7d46d260a3bf313512a5b5fc77e6c7d8a8aae0b8056c3dc4d8", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-final-20261014T155024Z/career.trx", + "bytes": 19180, + "sha256": "4d34e2045ced1a0b2bd3251893a5cad0e012c396fabd8b989466a84b2365b139", + "hashPolicy": "Exact raw bytes" + } + ], + "qualification": "12ordinaryPASS:11loopbackHTTP/nativeSQLite cases with controlled fixed test claims, one actual unexecuted MariaDB command/query composition policy. Real provider execution and productionJWT are not inferred from this fixture." + }, + "relatedBackend": { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-build", + "--filter", + "FullyQualifiedName~SqliteDateTimeOffsetCompatibilityTests|FullyQualifiedName~ApplicationAssetsTests|FullyQualifiedName~ApplicationWorkspaceTests|FullyQualifiedName~SubmittedApplicationPackageTests|FullyQualifiedName~AttachmentsControllerTests|FullyQualifiedName~ApiAuthorizationPostureTests|FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~EmailDraftsControllerTests|FullyQualifiedName~EmailDraftPersistenceTests|FullyQualifiedName~EmailSendControllerTests|FullyQualifiedName~EmailSendAttemptStoreTests", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-related-security-final-20261014T155248Z" + ], + "startedUtc": "2026-10-14T15:52:48.930990+00:00", + "finishedUtc": "2026-10-14T15:52:53.240041+00:00", + "exitCode": 0, + "logSha256": "e0f76be4231cf35387991304e41cdd6a0ad6ffa5b3662d7dbe61ee568bcd61c2", + "counts": { + "total": 80, + "passed": 80, + "failed": 0, + "notExecuted": 0 + }, + "resultSha256": "0fcf921713577adf3fb0a03d15162efaab54d561afcfc7709ade8127f8a99f1d" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-related-security-final-20261014T155248Z/command.json", + "bytes": 1385, + "sha256": "2789da16cca83848a6068c28e4e9de2fe46d09cb18578109bb06d36e1b9699d3", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-related-security-final-20261014T155248Z/career.log", + "bytes": 624, + "sha256": "e0f76be4231cf35387991304e41cdd6a0ad6ffa5b3662d7dbe61ee568bcd61c2", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-related-security-final-20261014T155248Z/career.trx", + "bytes": 115033, + "sha256": "0fcf921713577adf3fb0a03d15162efaab54d561afcfc7709ade8127f8a99f1d", + "hashPolicy": "Exact raw bytes" + } + ], + "caseInventory": { + "EmailDraftsControllerTests": 12, + "ApplicationAssetsTests": 15, + "SubmittedApplicationPackageTests": 4, + "EmailSendControllerTests": 7, + "SessionContextMiddlewareTests": 13, + "EmailSendAttemptStoreTests": 5, + "AttachmentsControllerTests": 3, + "ApiAuthorizationPostureTests": 5, + "ApplicationWorkspaceTests": 10, + "SqliteDateTimeOffsetCompatibilityTests": 5, + "EmailDraftPersistenceTests": 1 + }, + "qualification": "80existingordinaryPASS, samefinalReleaseDLL/no-build:37 document/archive/SQLite contracts,25draft/send guards,18authposture/session. Not additive cases or whole backend acceptance." + }, + "native": { + "documents-contract": { + "command": { + "name": "documents-contract", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "e2e/phase7-documents-contract.spec.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\documents-contract-results" + ], + "startedUtc": "2026-10-14T15:54:17.479545+00:00", + "finishedUtc": "2026-10-14T15:54:29.733344+00:00", + "exitCode": 0 + }, + "commandRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/documents-contract-command.json", + "sha256": "296e077bbdd1137ceba19e8f8b483ccd2a006c7728cd012f30c4751eedaec90b", + "bytes": 497, + "hashPolicy": "Exact measured raw bytes" + }, + "reportRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/documents-contract.json", + "sha256": "1527020501855bcc5e5733bc680d3fba8e6719a035b7e4948b7d7c2097f430a9", + "bytes": 48386, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/documents-contract.log", + "sha256": "bb4da25989d80fbefa62fb4a0029704a5c27d77f5953f28e52f8ffb79fdfd64e", + "bytes": 1129, + "hashPolicy": "Exact measured raw bytes" + }, + "stats": { + "startTime": "2026-10-14T15:54:18.126Z", + "duration": 11565.979000000001, + "expected": 3, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "[P7-07 documents contract] actual owner metadata has exact27 keys, literal Nordic search, intersecting filters and stable paginated rereads", + "file": "phase7-documents-contract.spec.ts", + "line": 58, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 1720 + }, + { + "title": "[P7-07 documents contract] actual cookies require authentication and isolate owner metadata despite forged owner or foreign-parent filters", + "file": "phase7-documents-contract.spec.ts", + "line": 72, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 821 + }, + { + "title": "[P7-07 documents contract] existing CV and email revisions detach and deletes preserve projection identities without metadata-list writes", + "file": "phase7-documents-contract.spec.ts", + "line": 80, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 909 + } + ], + "reportErrors": [], + "configuration": { + "version": "1.62.1", + "workers": 1, + "projects": [ + { + "name": "chromium", + "retries": 0 + } + ] + }, + "audit": { + "scans": [], + "violationNodes": 0, + "incompleteNodes": 0, + "checkContexts": [], + "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending." + } + }, + "application-p2d01": { + "command": { + "name": "application-p2d01", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "e2e/redesign-preservation.spec.ts", + "--grep", + "legacy workspace preserves frozen documents", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\application-p2d01-results" + ], + "startedUtc": "2026-10-14T15:54:29.738467+00:00", + "finishedUtc": "2026-10-14T15:54:43.075754+00:00", + "exitCode": 0 + }, + "commandRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/application-p2d01-command.json", + "sha256": "999f173b1e75f9c0fa7928244f8f9fe25b15c04c74205c040b7ea816e8241d1d", + "bytes": 558, + "hashPolicy": "Exact measured raw bytes" + }, + "reportRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/application-p2d01.json", + "sha256": "05ab995fa44c0e6c885a9b8fc2624acf294aaac4fb6179e06430f4be4d610f10", + "bytes": 150244, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/application-p2d01.log", + "sha256": "c4e621a5fbcb548dda34d89970fe81c75bd883b8f7287432659f2aaf1928b33b", + "bytes": 674, + "hashPolicy": "Exact measured raw bytes" + }, + "stats": { + "startTime": "2026-10-14T15:54:30.305Z", + "duration": 12729.931, + "expected": 1, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "file": "redesign-preservation.spec.ts", + "line": 73, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 4613 + } + ], + "reportErrors": [], + "configuration": { + "version": "1.62.1", + "workers": 1, + "projects": [ + { + "name": "chromium", + "retries": 0 + } + ] + }, + "audit": { + "scans": [], + "violationNodes": 0, + "incompleteNodes": 0, + "checkContexts": [], + "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending." + } + } + }, + "immutableNativeCapsule": { + "path": ".artifacts/phase7/p7-07/native-final/report/results.json", + "bytes": 50448, + "sha256": "baa1d592e5408220ed9d6f9742af907014c275cbbcb3ad76fb7117435ace345e", + "hashPolicy": "Exact raw bytes" + }, + "gates": [ + { + "command": { + "name": "browser-typecheck", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "exec", + "tsc", + "--", + "--project", + "../.artifacts/phase7/e2e-tsconfig.json", + "--noEmit" + ], + "startedUtc": "2026-10-14T11:25:11.856886+00:00", + "finishedUtc": "2026-10-14T11:25:16.962096+00:00", + "exitCode": 0 + }, + "commandRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/gates/browser-typecheck-command.json", + "sha256": "0f32835a896b72b0fbb13840432fa04bc76e13fbaebb4651caf314241c754f5f", + "bytes": 343, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-07/native-final/report/gates/browser-typecheck.log", + "sha256": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6", + "bytes": 78, + "hashPolicy": "Exact measured raw bytes" + } + } + ], + "sourceBoundary": [ + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-final/pre-source.json", + "bytes": 8178, + "sha256": "9f2036dabb4bafdf16cac949fe08be725ce771767ab72347d62668e72a9c0505", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-final/post-source.json", + "bytes": 5545, + "sha256": "708f70a6ae88e1c814dd2f9f0c4e2adb741267cae08b181eccc5a4586db26c36", + "hashPolicy": "Exact raw bytes" + } + }, + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-related-final/pre-source.json", + "bytes": 11057, + "sha256": "ff058f49bbd7746ceaa05a4620711dbf1430b695afd003fa7533ac377232e720", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-related-final/post-source.json", + "bytes": 6281, + "sha256": "9a3bfdd2069672b46159042e9d6e3ed13912a52880a5903c8c94c9bad20f7274", + "hashPolicy": "Exact raw bytes" + } + }, + { + "pre": { + "path": ".artifacts/phase7/p7-07/native-final/pre-source.json", + "bytes": 7814, + "sha256": "7e6d9212c76fffa6395d58ea4b0055a7e30190b7a808a9222f01025c3cf82d3d", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/native-final/post-source.json", + "bytes": 4376, + "sha256": "b801ccb9fea98d7af66c6c68daac57600719d7e8b524aa64257a49dd2f97a6d0", + "hashPolicy": "Exact raw bytes" + } + } + ], + "sources": [ + { + "originalSourcePath": "JobTrackerApi/Models/CvVariant.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/CvVariant.cs", + "sha256": "9032bd64091d5162e8998b4d117cd0a653323b22ae9653edf0e33566c25807ba", + "bytes": 2528, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/CvVariant.cs", + "sha256": "9032bd64091d5162e8998b4d117cd0a653323b22ae9653edf0e33566c25807ba", + "bytes": 2528, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/CoverLetterVersion.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/CoverLetterVersion.cs", + "sha256": "96adb0b37a80533a65688ab28b0e9432616ef67d2b2eec0224a76fb3c6caf0bb", + "bytes": 1724, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/CoverLetterVersion.cs", + "sha256": "8648c1d567584e6f353c0e470997b3d201ccaa30ec30b513bb4d9a7f02498ffe", + "bytes": 1683, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/Attachments.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/Attachments.cs", + "sha256": "23dc6834f533b319fcdb7d908bdc67b2d77702a7b888aaed51f80e30ba6ca581", + "bytes": 680, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/Attachments.cs", + "sha256": "23dc6834f533b319fcdb7d908bdc67b2d77702a7b888aaed51f80e30ba6ca581", + "bytes": 680, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/SubmittedApplicationPackage.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/SubmittedApplicationPackage.cs", + "sha256": "5baac5a52cadda819ce10c818226f6debcf5c4c8845dc3418430c870ae3cbb0a", + "bytes": 1727, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/SubmittedApplicationPackage.cs", + "sha256": "e24feca929d72e0d044f5b68f0493049658cc7ff96a8a6805e4b9b8363ab89c8", + "bytes": 1690, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/EmailDraft.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/EmailDraft.cs", + "sha256": "546f51251d3743cae68149c656366c0ead74aa1ea22d35318508b5ec2ce17671", + "bytes": 1047, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/EmailDraft.cs", + "sha256": "ad775240255512aec5ec33c7c15639235c231cd9432487c646ab8d76b7b7f6aa", + "bytes": 1016, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/JobApplication.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/JobApplication.cs", + "sha256": "dd75f1e33d5094e0c244938365e1a3ff63a1ecf1895247a8008dd0ec667904d1", + "bytes": 4147, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/JobApplication.cs", + "sha256": "acba81ce225265b9af0fe0dc2bae52020a556c25552dc86d3c0919127ab1c3da", + "bytes": 4059, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/TailoredCvDraft.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/TailoredCvDraft.cs", + "sha256": "5d3427ce02cc5a5763b752c27b338eae6f561f6b00f6902e6bf8481d1f7f5e90", + "bytes": 2809, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/TailoredCvDraft.cs", + "sha256": "1f0b283ef416f7776a6e029c29461c624b7a02d9a97d17254b968daa6933697b", + "bytes": 2738, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Data/JobTrackerContext.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Data/JobTrackerContext.cs", + "sha256": "d755c97ad025d09e06e92e4d338be8cfbb332ea13e2fd4c32046fe2763a006be", + "bytes": 37702, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Data/JobTrackerContext.cs", + "sha256": "006e025533da2a110edeb6ef9f6c33fa651b7fb3dc018c36be40d7efbf1250cc", + "bytes": 37068, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/JobApplicationHelpers.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/JobApplicationHelpers.cs", + "sha256": "61b04b3d3bd040129510eaef66eef6f14fd4455593ffbecb42f34319aff16d5d", + "bytes": 31076, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/JobApplicationHelpers.cs", + "sha256": "2a51e1edb8ce43f987b2381e35f2495e42c078bc69838b31b1a3c645717d90cb", + "bytes": 30438, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/CvVariantService.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/CvVariantService.cs", + "sha256": "97f450fc3613c03de064816e6388a8cfab60d46db5d2739ed3e9ba5da7a71bc2", + "bytes": 13061, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/CvVariantService.cs", + "sha256": "e9b12b9bb23dee29b72c716a07976a7f2b4d1817999e6a594184b1f60c2b1f2b", + "bytes": 12824, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/SubmittedApplicationPackageService.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/SubmittedApplicationPackageService.cs", + "sha256": "4f1b6a8f1f72dfe1f3f77bcd1b68e326d2b9eed5cacd72e6b3e54263ff15da5c", + "bytes": 8750, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/SubmittedApplicationPackageService.cs", + "sha256": "724f8e5d5a4398d64c7248abc2df4de1d4901e55bfd1393c8d8c902788052727", + "bytes": 8574, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/ApplicationAssetsService.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/ApplicationAssetsService.cs", + "sha256": "0eeb42c046bc0724acc93eff31275b156f69f7e8d6c6200ee620a3d5011bacb4", + "bytes": 15548, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/ApplicationAssetsService.cs", + "sha256": "9e98be758c86dc95738ce398760ab70077d76ec75315c2c271d0995e0e46a13b", + "bytes": 15221, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/ApplicationWorkspaceService.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/ApplicationWorkspaceService.cs", + "sha256": "89804f852b410b6b5f357ee202ba4d11974f5b795679ef6b6978fb140fb936a5", + "bytes": 7343, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/ApplicationWorkspaceService.cs", + "sha256": "916f4a0bfadf3e444090565c893a045ec5a7d690b4db18db73c53dc85f6b5024", + "bytes": 7179, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Controllers/AttachmentsController.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/AttachmentsController.cs", + "sha256": "ac5de30857d39bd9f03409d43e1bf2272af4d5a98bd6505c913405d2bde5b82f", + "bytes": 18173, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/AttachmentsController.cs", + "sha256": "9faf924f7d756ad1ec4bd174b1e175bbef57ad5f6444e769db590784a58188c9", + "bytes": 17780, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Controllers/EmailDraftsController.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/EmailDraftsController.cs", + "sha256": "bcbb26145668bb82666d43e1721418864220a15149487282af40397cd86784a5", + "bytes": 11172, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/EmailDraftsController.cs", + "sha256": "a4c563e6266c944a9be467f4a8278cacc6e9a3ebc1625cb5b1fe791a2ff3887f", + "bytes": 10932, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Program.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Program.cs", + "sha256": "e906d23d8d4195a8e361428b26a9a713a77981f4d6636c80f68f6706c47e4d53", + "bytes": 32013, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Program.cs", + "sha256": "6a548ddf7bc311840b931deaa89d21f75f01ac1f48d09699e4b5cd904f716432", + "bytes": 31366, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Controllers/DocumentsController.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/DocumentsController.cs", + "sha256": "44633b84fbd33116e7f79f0fcd77cc8c15aea86d752464c6940b7dcd5506548b", + "bytes": 1711, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/DocumentsController.cs", + "sha256": "44633b84fbd33116e7f79f0fcd77cc8c15aea86d752464c6940b7dcd5506548b", + "bytes": 1711, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/DocumentsIndexService.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/DocumentsIndexService.cs", + "sha256": "def370bd0dbe90260e42800fded99302e56f8ac6b7693e73dd0a60cf0a3f7fd1", + "bytes": 14996, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/DocumentsIndexService.cs", + "sha256": "def370bd0dbe90260e42800fded99302e56f8ac6b7693e73dd0a60cf0a3f7fd1", + "bytes": 14996, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Controllers/EmailController.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/EmailController.cs", + "sha256": "75b01564daa046c51a28bf15688770c6f71d168cf17efe6ed52d5e7894d88929", + "bytes": 14133, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Controllers/EmailController.cs", + "sha256": "06582547ffe17631563f39b9409cf00de5b8b98c1aca48bf3d5dad5e7a079246", + "bytes": 13846, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Services/EmailSendAttemptStore.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/EmailSendAttemptStore.cs", + "sha256": "4fb3e7ad8a8534cfbc081c42114bbf2fce65b954d561f6cff96d3548bc346f13", + "bytes": 9777, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Services/EmailSendAttemptStore.cs", + "sha256": "efaafea9e0b845f31d54b6472d59c15b42d992705a99c3413f2b219d3ac452e2", + "bytes": 9599, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "JobTrackerApi/Models/EmailSendAttempt.cs", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/EmailSendAttempt.cs", + "sha256": "47c1f579fd72d37dd9e020b4dd11d8c10cbfa5741e04076fcbd9c73ef579e062", + "bytes": 1373, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/JobTrackerApi/Models/EmailSendAttempt.cs", + "sha256": "54bbe297c3397e7240ed5ccb307c0212dc6696ecee8c1561410eb4f33aa19a61", + "bytes": 1334, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "job-tracker-ui/e2e/phase7-documents-contract.spec.ts", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/job-tracker-ui/e2e/phase7-documents-contract.spec.ts", + "sha256": "aca359ec044332ed688bd108352c28d6f4c960c8201dd4a7853b6281d341f483", + "bytes": 18346, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/job-tracker-ui/e2e/phase7-documents-contract.spec.ts", + "sha256": "aca359ec044332ed688bd108352c28d6f4c960c8201dd4a7853b6281d341f483", + "bytes": 18346, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + }, + { + "originalSourcePath": "job-tracker-ui/e2e/redesign-preservation.spec.ts", + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/job-tracker-ui/e2e/redesign-preservation.spec.ts", + "sha256": "997b01c7e2a313e5f590820bad6b630af7eec67e782df7a786ccce6ebd569da0", + "bytes": 23275, + "hashPolicy": "Exact measured raw bytes" + }, + "canonicalLf": { + "path": ".artifacts/phase7/p7-07/native-final/report/source/job-tracker-ui/e2e/redesign-preservation.spec.ts", + "sha256": "fd96f798f33badd676c5850f65b306ad171af69bfc5644e35760482518c25105", + "bytes": 22991, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance." + } + ], + "verifiedFileReferences": 137, + "frontend": { + "buildId": "LFCrTZzDtX5hNBSRBj5Xx", + "export": { + "count": 74, + "sha256": "569954e8909ed96298d4aa7c219a60cf2c35c22bc05c3fd009f3fa77b5404962", + "policy": "Windows Path sort; original output path + TAB + exact raw file SHA + LF" + }, + "qualification": "Frontend product unchanged. P7-06 export is launch fixture for API-only3; existingworkspace used byP2-D01. Browsertypes11:25pass with heldaca native spec, no new frontendproductbuild or Jest execution required by backend-only change." + }, + "visuals": { + "retained": 1, + "reviewed": 1, + "images": [ + { + "raw": { + "path": ".artifacts/phase7/p7-07/native-final/report/embedded/application-p2d01/001-native-frozen-package.png", + "sha256": "e37f5f0c59e267f37e3dcb54d992b626f3329dcf13c57860dd5d7cd6d6226dd4", + "bytes": 109364, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-07/native-final/report/application-p2d01.json", + "sha256": "05ab995fa44c0e6c885a9b8fc2624acf294aaac4fb6179e06430f4be4d610f10", + "bytes": 150244, + "hashPolicy": "Exact measured raw bytes" + }, + "lane": "application-p2d01", + "test": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", + "name": "native-frozen-package", + "manualReview": "ACTUALLY_DISPLAYED_BY_ROOT" + } + ], + "qualification": "Existing P2-D01 frozen-package consumer screenshot, no DocumentsUI image. Opacity transition/full-page sticky capture limits; not settledvisual/contrast/rendering-fidelity certification." + }, + "accessibility": { + "newDocumentsUIScans": 0, + "status": "UI_NOT_STARTED", + "qualification": "API contract does not establish responsive/axe/humanreader/device/zoom acceptance; UI checkpoint follows verified contract push." + }, + "history": [ + { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-restore", + "--filter", + "FullyQualifiedName~DocumentsControllerTests", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-documents-red-20261014T111234Z" + ], + "startedUtc": "2026-10-14T11:12:34.372367+00:00", + "finishedUtc": "2026-10-14T11:12:50.335904+00:00", + "exitCode": 1, + "logSha256": "928a0156329e379ba536b735be49d398d80a3d36d85e06618ded4904cf22de12", + "counts": { + "total": 11, + "passed": 0, + "failed": 11, + "notExecuted": 0 + }, + "resultSha256": "bd914741dc627466c20d74347a3df17dd42011914b9dfeffe53bdc6314846616" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-red-20261014T111234Z/command.json", + "bytes": 900, + "sha256": "cb95c3165b72cf3c5ca8401d3d6cbcfabe36ec7ffc688a1bc9d82dde9af145e3", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-red-20261014T111234Z/career.log", + "bytes": 13149, + "sha256": "928a0156329e379ba536b735be49d398d80a3d36d85e06618ded4904cf22de12", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-red-20261014T111234Z/career.trx", + "bytes": 42815, + "sha256": "bd914741dc627466c20d74347a3df17dd42011914b9dfeffe53bdc6314846616", + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-restore", + "--filter", + "FullyQualifiedName~DocumentsControllerTests.MariaDb_composition", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-provider-policy-red-20261014T112750Z" + ], + "startedUtc": "2026-10-14T11:27:50.588566+00:00", + "finishedUtc": "2026-10-14T11:28:02.188395+00:00", + "exitCode": 1, + "logSha256": "7b8818228c9486f5e49597804122a3adf7419be77d94a9f67476fe0231c23c29", + "counts": { + "total": 1, + "passed": 0, + "failed": 1, + "notExecuted": 0 + }, + "resultSha256": "c6fca622a09e8c85971514e7b1ca70ba81b99db645d55c87c3a3ce7f9597a0da" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-provider-policy-red-20261014T112750Z/command.json", + "bytes": 924, + "sha256": "042f3e7cea069b61febbc63c7114f0ab08f8225365be3d7009e64671a3c74cb9", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-provider-policy-red-20261014T112750Z/career.log", + "bytes": 1970, + "sha256": "7b8818228c9486f5e49597804122a3adf7419be77d94a9f67476fe0231c23c29", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-provider-policy-red-20261014T112750Z/career.trx", + "bytes": 5105, + "sha256": "c6fca622a09e8c85971514e7b1ca70ba81b99db645d55c87c3a3ce7f9597a0da", + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-restore", + "--filter", + "FullyQualifiedName~DocumentsControllerTests", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-documents-green-candidate-1-20261014T113029Z" + ], + "startedUtc": "2026-10-14T11:30:29.637426+00:00", + "finishedUtc": "2026-10-14T11:30:41.875848+00:00", + "exitCode": 1, + "logSha256": "7e7542bc8c2a1da78deadf0809572251e3aed97eae155f37550e2eb7e9ce1d60", + "counts": { + "total": 12, + "passed": 10, + "failed": 2, + "notExecuted": 0 + }, + "resultSha256": "e97c2250003429562a5542174a1aea03ec3005c77590fc1b94056f37d3689a1c" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-green-candidate-1-20261014T113029Z/command.json", + "bytes": 914, + "sha256": "6c8194e12f28f8bf7bdba98a408ef4614d71bf10a354f71104e0a350f59027bf", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-green-candidate-1-20261014T113029Z/career.log", + "bytes": 2933, + "sha256": "7e7542bc8c2a1da78deadf0809572251e3aed97eae155f37550e2eb7e9ce1d60", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-green-candidate-1-20261014T113029Z/career.trx", + "bytes": 23098, + "sha256": "e97c2250003429562a5542174a1aea03ec3005c77590fc1b94056f37d3689a1c", + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-restore", + "--filter", + "FullyQualifiedName~DocumentsControllerTests.Cleared_letter_history|FullyQualifiedName~DocumentsControllerTests.MariaDb_composition", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-diagnostic-2-20261014T154117Z" + ], + "startedUtc": "2026-10-14T15:41:17.837210+00:00", + "finishedUtc": "2026-10-14T15:41:31.709973+00:00", + "exitCode": 1, + "logSha256": "73eec678fb39d80446c56c195fda2267a7a218c033a2d9375d06b3f50a7838c3", + "counts": { + "total": 2, + "passed": 0, + "failed": 2, + "notExecuted": 0 + }, + "resultSha256": "a47babbe906616b17431ea8f6a690730e3e6b76227b12a8f87594accb4f81787" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-diagnostic-2-20261014T154117Z/command.json", + "bytes": 984, + "sha256": "f02e1f408dc95d6d39ad475c8f55fcc37ece0845f444bf8701e3167f8710df68", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-diagnostic-2-20261014T154117Z/career.log", + "bytes": 13559, + "sha256": "73eec678fb39d80446c56c195fda2267a7a218c033a2d9375d06b3f50a7838c3", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-diagnostic-2-20261014T154117Z/career.trx", + "bytes": 33668, + "sha256": "a47babbe906616b17431ea8f6a690730e3e6b76227b12a8f87594accb4f81787", + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-restore", + "--filter", + "FullyQualifiedName~DocumentsControllerTests", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-documents-green-candidate-3-20261014T154719Z" + ], + "startedUtc": "2026-10-14T15:47:19.771612+00:00", + "finishedUtc": "2026-10-14T15:47:36.190838+00:00", + "exitCode": 0, + "logSha256": "243967d5225dd0eb398ae6b0216a944e4519a4047fb7fb33530699059fa4afd9", + "counts": { + "total": 12, + "passed": 12, + "failed": 0, + "notExecuted": 0 + }, + "resultSha256": "cf5791cef3e159d144cb5a4b0628beeba78562d1975676108458005e028e2277" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-green-candidate-3-20261014T154719Z/command.json", + "bytes": 914, + "sha256": "2bdc4912b5fc46e643aca0f87ce8add9c40bd031505fe960eb7df9767a821a85", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-green-candidate-3-20261014T154719Z/career.log", + "bytes": 1000, + "sha256": "243967d5225dd0eb398ae6b0216a944e4519a4047fb7fb33530699059fa4afd9", + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-documents-green-candidate-3-20261014T154719Z/career.trx", + "bytes": 19180, + "sha256": "cf5791cef3e159d144cb5a4b0628beeba78562d1975676108458005e028e2277", + "hashPolicy": "Exact raw bytes" + } + ] + } + ], + "nativeRed": { + "path": ".artifacts/phase7/p7-07/native-red-old-api/report/results.json", + "bytes": 16128, + "sha256": "a67a8152b6bbff140868f9414d0381d86f9f1089cc726b33caa892827bb20c39", + "hashPolicy": "Exact raw bytes" + }, + "nativeRedSourceReceipt": { + "path": ".artifacts/phase7/p7-07/native-red-old-api/pre.json", + "bytes": 1485, + "sha256": "7b458cca8e26f9a428222cd54c4d2dca37ff611c5d260804733e2f14b3b8740f", + "hashPolicy": "Exact raw bytes" + }, + "nativeRedCaptureCorrection": { + "path": ".artifacts/phase7/p7-07/native-red-old-api/capture-qualification.json", + "bytes": 558, + "sha256": "32e64300bae5b9c00cf1724c261bc6eee6db4b32acb38742868d0ab13b52b9cf", + "hashPolicy": "Exact raw bytes" + }, + "historySource": [ + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-red/pre-source.json", + "bytes": 6837, + "sha256": "3d4eed5b62d962735878173d11b9aafaf66387bb67853f8cdcd780acff0d851e", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-red/post-source.json", + "bytes": 4561, + "sha256": "92279d7ec16184f7792e828e19423b5049cd26c97e26f061ccb85d20ac40e4fd", + "hashPolicy": "Exact raw bytes" + } + }, + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-provider-red/pre-source.json", + "bytes": 7662, + "sha256": "cc06010d6823473da7ecc8c8aca08489465a5d56cf8cfe14e3bc491ad23eecd9", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-provider-red/post-source.json", + "bytes": 4560, + "sha256": "fda97b9647ffba138ee9df77aab5e3a69b65df5041440f7084488de99faa7dbc", + "hashPolicy": "Exact raw bytes" + } + }, + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-green-candidate-1/pre-source.json", + "bytes": 8521, + "sha256": "1aafe6088c32b362d203da7359666c83c5e7f967066413ac81642d86d33c65ca", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-green-candidate-1/post-source.json", + "bytes": 4943, + "sha256": "e72936b3dbae3f0fad5f5ffc23f524cd52ce2aef36426a8ba6a78fbef6252436", + "hashPolicy": "Exact raw bytes" + } + }, + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-diagnostic-2/pre-source.json", + "bytes": 8345, + "sha256": "3a6d74215a45cba80f4a7f5bc6dcc7a27097e254d72dbdc612964a47643f116d", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-diagnostic-2/post-source.json", + "bytes": 4943, + "sha256": "e25372027f81c193c524f8809df9ed2f13266b9693125ed520dfc393f581f643", + "hashPolicy": "Exact raw bytes" + } + }, + { + "pre": { + "path": ".artifacts/phase7/p7-07/contract-green-candidate-3/pre-source.json", + "bytes": 8472, + "sha256": "c0f8bb4b9fd1f748157266a6f97b029aed5bf3bb97e784e9adda8d9988224dcf", + "hashPolicy": "Exact raw bytes" + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-green-candidate-3/post-source.json", + "bytes": 5030, + "sha256": "0a4c2f5c26ad795804d9524924d83dff3c52696e04a5259f1d7a3346d4965694", + "hashPolicy": "Exact raw bytes" + } + } + ], + "corrections": "Allfailed attempts retained. InitialHTTP11 andactualProgram3 fail missing404 before later assertions. MariaDB modernmarkerpolicy lacked ordinalUnicodecollation:CONVERTutf8mb4/utf8mb4_bin preservescharacterpositions. WindowsEFSQLlineformatting rewrote literalLF/CR matchvalues, exposing phantomempty material5/7; sharedCHAR9-13 fix passedstrict4IDs and fullstandard.NETwhitespace fixture. ToQueryString rendersMariaDBLIMIT values for display; actualunexecutedDbCommand preservesdistinctbound7/14 parameters. No skips/retries/expected-failure markers orrelaxedpayload/tenant/history/assertions. Oldnativebinary/spec receipt11:26:04 wasAFTERrun11:25:28; rawDURINGlabelcorrected explicitly, no fabricatedpre/duringcapture.", + "contract": "GET/api/documents:27publicmetadatafields acrosssevenexistingresourcekinds;2parameterizedSELECTcount+boundedpage;authenticatedowner ineachbranch/parent/company;literalmetadata-onlyNordicsearch;validatedfilters/name-date sorts;ownedtrashedresources/detachedCV/history-onlyletter/capturedpackage preserved;no body/settings/profile/HTML/path/bytes/address/thread/sendingUUID output;no entitywholecollection/materializedbody/storage/renderer/AI/capture/listwrites.", + "datePolicy": "ExactUTCintegeroffset/fractionordering, knowncomparableUTCdatesfirst. Uploaded-local wall-clock date withtimezoneunrecorded retained; UTCsortkeynull, unknown/local lastinbothdirections, stabletype/resource ties. No fabricatedUTC or genericlastupdated claim.", + "actions": "Sourcesremainexistingowners. EmailCorrespondence andpackageSubmission are context links, notexactresume/selectedsnapshot. CVPDF remains explicitPOST atsource; no fakeGETdownload. AttachmentdownloadHref isAPIpath/api/attachments/download/id; Axiosbasealready/api mustuseexistingrelativeauthenticatedhelper. No genericDocumentmodel/storage/schema orfile relocation.", + "countProvenance": "12newbackendcases derive1053 from1041; three newordinarynative cases derive304 from301; frontend739/94unchanged. Actualwhole573/89frontend,1033backend,49sidecar inherited untilP7-12. Related80andexistingP2-D01 are notnewcases.", + "preservation": { + "groups": 57, + "partial": 56, + "deferred": [ + "PRES-C11" + ], + "originalRoutes": 32, + "currentFrontendRoutes": 32, + "references": [ + "PRES-C05", + "PRES-C06", + "PRES-C07", + "PRES-C08", + "PRES-C09", + "PRES-C10", + "PRES-C12", + "PRES-W05", + "PRES-W06", + "PRES-W07", + "PRES-W08", + "PRES-M04", + "PRES-S05", + "PRES-E09" + ], + "qualification": "Finiteindexcontracts strengthened; no whole-groupPASS, no existingroute/sourceaction/ownership/data removed. OrdinaryP2-D01 freshlypasses;renderer/frozen sourcebytes remainunchanged." + }, + "review": "Independent connected-source reviewer found no criticalconcrete contract issue; no runtimeclaims fromreview. FinalSource/runtime/static/push checks remainindependent.", + "remaining": "DocumentsUI nextaftercontractpush. ActualMariaDBexecution/performance unverified; opaquequerycomposition cannotcertifySQLserversemantics. Humanreader/device/zoom/fonts/photo/print/provider and productionF04/backup/schema/config/rollback/otherdebt retained. P513/C11withheld32/366/0calls/0human;06Fdeferred. Main27657f3unchanged, productionNO-GO, noPhase8.", + "finalHygiene": { + "schemaVersion": 1, + "checkpoint": "p7-07", + "status": "PASS", + "checkedUtc": "2026-10-14T16:00:00.842457+00:00", + "commands": [ + { + "name": "secret-scan", + "command": [ + "rtk", + "proxy", + "C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe", + "-X", + "utf8", + "scripts/supply-chain.py", + "scan-secrets" + ], + "startedUtc": "2026-10-14T15:59:56.613262+00:00", + "finishedUtc": "2026-10-14T16:00:00.024723+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/secret-scan.log", + "logSha256Raw": "c5a88d5ebdc042adba125c1eaacbe44f9f4a5c19898c0f195573c00d511aa28d" + }, + { + "name": "working-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--check" + ], + "startedUtc": "2026-10-14T16:00:00.025224+00:00", + "finishedUtc": "2026-10-14T16:00:00.135215+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/working-whitespace.log", + "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6" + }, + { + "name": "staged-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--cached", + "--check" + ], + "startedUtc": "2026-10-14T16:00:00.136212+00:00", + "finishedUtc": "2026-10-14T16:00:00.244002+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/staged-whitespace.log", + "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6" + }, + { + "name": "phase7-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--check", + "b5319981ad3d1bd635daff861453d23c0de5320b" + ], + "startedUtc": "2026-10-14T16:00:00.244505+00:00", + "finishedUtc": "2026-10-14T16:00:00.427465+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/phase7-whitespace.log", + "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6" + } + ], + "untrackedSecretScan": { + "files": 0, + "findings": 0 + }, + "documentation": { + "files": 18, + "relativeLinks": 504, + "errors": [] + }, + "preservation": { + "groups": 57, + "statuses": { + "PARTIAL": 56, + "PASS": 0, + "FAIL": 0, + "DEFERRED": 1 + }, + "originalRoutesPreserved": true, + "currentConcreteRoutes": 32 + }, + "production": { + "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "status": "NO-GO", + "qualification": "Local fetched refs; direct push observation recorded separately." + }, + "hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.", + "limits": [ + "Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.", + "Local links check target existence, not URL/heading anchors.", + "Static evidence is not runtime/manual/device/provider/production acceptance." + ] + } +} diff --git a/docs/redesign/phase-7/p7-07/evidence/contract-static-checks.json b/docs/redesign/phase-7/p7-07/evidence/contract-static-checks.json new file mode 100644 index 0000000..260f9f7 --- /dev/null +++ b/docs/redesign/phase-7/p7-07/evidence/contract-static-checks.json @@ -0,0 +1,104 @@ +{ + "schemaVersion": 1, + "checkpoint": "p7-07", + "status": "PASS", + "checkedUtc": "2026-10-14T16:00:00.842457+00:00", + "commands": [ + { + "name": "secret-scan", + "command": [ + "rtk", + "proxy", + "C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe", + "-X", + "utf8", + "scripts/supply-chain.py", + "scan-secrets" + ], + "startedUtc": "2026-10-14T15:59:56.613262+00:00", + "finishedUtc": "2026-10-14T16:00:00.024723+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/secret-scan.log", + "logSha256Raw": "c5a88d5ebdc042adba125c1eaacbe44f9f4a5c19898c0f195573c00d511aa28d" + }, + { + "name": "working-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--check" + ], + "startedUtc": "2026-10-14T16:00:00.025224+00:00", + "finishedUtc": "2026-10-14T16:00:00.135215+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/working-whitespace.log", + "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6" + }, + { + "name": "staged-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--cached", + "--check" + ], + "startedUtc": "2026-10-14T16:00:00.136212+00:00", + "finishedUtc": "2026-10-14T16:00:00.244002+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/staged-whitespace.log", + "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6" + }, + { + "name": "phase7-whitespace", + "command": [ + "rtk", + "proxy", + "git", + "diff", + "--check", + "b5319981ad3d1bd635daff861453d23c0de5320b" + ], + "startedUtc": "2026-10-14T16:00:00.244505+00:00", + "finishedUtc": "2026-10-14T16:00:00.427465+00:00", + "exitCode": 0, + "log": ".artifacts/phase7/current/static/20261014T155956Z/phase7-whitespace.log", + "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6" + } + ], + "untrackedSecretScan": { + "files": 0, + "findings": 0 + }, + "documentation": { + "files": 18, + "relativeLinks": 504, + "errors": [] + }, + "preservation": { + "groups": 57, + "statuses": { + "PARTIAL": 56, + "PASS": 0, + "FAIL": 0, + "DEFERRED": 1 + }, + "originalRoutesPreserved": true, + "currentConcreteRoutes": 32 + }, + "production": { + "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "status": "NO-GO", + "qualification": "Local fetched refs; direct push observation recorded separately." + }, + "hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.", + "limits": [ + "Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.", + "Local links check target existence, not URL/heading anchors.", + "Static evidence is not runtime/manual/device/provider/production acceptance." + ] +} diff --git a/docs/redesign/phase-7/p7-07/evidence/entry.json b/docs/redesign/phase-7/p7-07/evidence/entry.json new file mode 100644 index 0000000..eb5470d --- /dev/null +++ b/docs/redesign/phase-7/p7-07/evidence/entry.json @@ -0,0 +1,188 @@ +{ + "schemaVersion": 1, + "checkpoint": "P7-07", + "recordedUtc": "2026-10-14T11:08:05.695242+00:00", + "entryCommit": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "precedingComposePush": { + "receipt": { + "path": ".artifacts/phase7/p7-06/compose/push-receipt.json", + "sha256": "983d817a38e40db0cfacf435e06381d9d1ba2aa71f369257b42fe6865b12d0cd", + "bytes": 2849 + }, + "observedUtc": "2026-10-14T11:03:15.576853+00:00", + "branch": "codex/ui-redesign-phase4", + "head": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "originDevelopment": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "liveDevelopment": "223dcf57d26bab624261994b61a46f2d3e2dd5b2", + "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "liveMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "workingTreeClean": true, + "pendingPaths": [], + "commands": [ + { + "command": [ + "rtk", + "proxy", + "git", + "branch", + "--show-current" + ], + "exitCode": 0, + "stdout": "codex/ui-redesign-phase4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "HEAD" + ], + "exitCode": 0, + "stdout": "223dcf57d26bab624261994b61a46f2d3e2dd5b2\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "origin/codex/ui-redesign-phase4" + ], + "exitCode": 0, + "stdout": "223dcf57d26bab624261994b61a46f2d3e2dd5b2\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "main" + ], + "exitCode": 0, + "stdout": "27657f31131cd95d42f0e1290853ec9c9b3959a4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "origin/main" + ], + "exitCode": 0, + "stdout": "27657f31131cd95d42f0e1290853ec9c9b3959a4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "ls-remote", + "origin", + "refs/heads/codex/ui-redesign-phase4", + "refs/heads/main" + ], + "exitCode": 0, + "stdout": "223dcf57d26bab624261994b61a46f2d3e2dd5b2\trefs/heads/codex/ui-redesign-phase4\n27657f31131cd95d42f0e1290853ec9c9b3959a4\trefs/heads/main\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "status", + "--porcelain=v1" + ], + "exitCode": 0, + "stdout": "", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + } + ] + }, + "baseline": { + "command": { + "command": [ + "rtk", + "proxy", + "C:/Users/Connor/.dotnet/dotnet.exe", + "test", + "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj", + "--configuration", + "Release", + "--no-build", + "--filter", + "FullyQualifiedName~SqliteDateTimeOffsetCompatibilityTests|FullyQualifiedName~ApplicationAssetsTests|FullyQualifiedName~ApplicationWorkspaceTests|FullyQualifiedName~SubmittedApplicationPackageTests|FullyQualifiedName~AttachmentsControllerTests", + "--logger", + "trx;LogFileName=career.trx", + "--results-directory", + "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p707-related-entry-223dcf5-20261014T110519Z" + ], + "startedUtc": "2026-10-14T11:05:19.519871+00:00", + "finishedUtc": "2026-10-14T11:05:23.691550+00:00", + "exitCode": 0, + "logSha256": "3c399e9c370ffc75330707fb1c0da5f5999a9cdf00925b98e1e03ca02f68b548", + "counts": { + "total": 37, + "passed": 37, + "failed": 0, + "notExecuted": 0 + }, + "resultSha256": "15a98e0c0c098020d601be386fb31055cffc7184122bdc573a8cfc53436cada8" + }, + "raw": [ + { + "path": ".artifacts/phase7/current/backend-career/p707-related-entry-223dcf5-20261014T110519Z/command.json", + "sha256": "753d9d668e0e98f783e95953dc5d567c748264484765bfecf143c6f51e6ea531", + "bytes": 1105 + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-related-entry-223dcf5-20261014T110519Z/career.log", + "sha256": "3c399e9c370ffc75330707fb1c0da5f5999a9cdf00925b98e1e03ca02f68b548", + "bytes": 623 + }, + { + "path": ".artifacts/phase7/current/backend-career/p707-related-entry-223dcf5-20261014T110519Z/career.trx", + "sha256": "15a98e0c0c098020d601be386fb31055cffc7184122bdc573a8cfc53436cada8", + "bytes": 52940 + } + ], + "pre": { + "path": ".artifacts/phase7/p7-07/contract-entry/pre-source.json", + "sha256": "0c4a5adce72df0f0f80986407de6ef9854c2d5abf430c61bab95721107e16ca2", + "bytes": 6342 + }, + "post": { + "path": ".artifacts/phase7/p7-07/contract-entry/post-source.json", + "sha256": "271f5ca6204600b3004b59a659c80dd84c6daf232fc9e8317e20f98db9347d6d", + "bytes": 3918 + }, + "qualification": "Fresh37case existingcontractsubset --no-build on unchanged951DLL compiledduringP7-06UTCgreen, not a new HEAD223 build or Documents endpoint test.20sourcepre/post and API bytes unchanged." + }, + "authoritativeDerivedInventory": { + "frontendCases": 739, + "frontendSuites": 94, + "browserCases": 301, + "backendCases": 1041, + "qualification": "Derived counts only; lastwhole573/89frontend1033backend49sidecar inherited untilP7-12." + }, + "scope": "Contractcommitfirst: metadataonlyownerSQLpagination over existing7resources. No UIuntilcontractverified/pushed; no genericDocumentmodel/storage/schema/render/AI/listingwrites.", + "datePolicy": "Comparable recordedUTCinstants first, exactoffset/fractionkeys; uploaded-localDateTime.Nowrawdate/timezoneunrecorded andunknownnullUTCsortlastinbothdirections. Neverclaimalllastupdated ormanufactureUTC.", + "preservation": { + "groups": 57, + "partial": 56, + "deferred": [ + "PRES-C11" + ], + "originalRoutes": 32 + }, + "remaining": "NativeHTTPREDthenboundedprojectionandGREEN+providercomposition, metadata/isolation/archiveproof, staticcommit/push, thenUI. RealMariaDBexecution/human/device/provider/output/productionnotclaimed;P513withheld06FdeferredmainunchangednoPhase8." +} diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md index eb96b0a..c85e900 100644 --- a/docs/roadmap/todo.md +++ b/docs/roadmap/todo.md @@ -90,7 +90,7 @@ Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.m - [x] P7-05 Discovery, Companies, Offers and Reminders: Discovery21/1,new8+existing4native/backend14/types/lint/build pass;18 actual new PNG reviews,81+52 axe incompletes retained. Discovery hygiene PASS/normal push34a4d6d verified. Companies30/4,new6+old2+named4native/backend2/types/lint/build pass;20actual PNG reviews,91+163incompletes qualified. Companies hygiene PASS/normal pusha8aba85 verified; Offers9/1,new4native/backend6/types/lint/build pass;14actual PNG reviews,15incompletes qualified. Offers hygiene PASS/normal pushc723a4e verified; Reminders17/3,new6native/backend6/types/lint/build pass;17actual PNG reviews/114incompletes qualified. P7-05 implemented CONDITIONAL GO, hygiene PASS/normal627955a push directly verified2026-10-13 11:49:32.473200UTC, clean localHEAD=origin=live/main unchanged. - [x] P7-06 [remaining Application forms and global correspondence](../redesign/phase-7/p7-06/README.md): all three mandatoryboundaries implemented CONDITIONAL GO; forms4c325/inboxa791 pushed, compose final hygiene/normal push pending. Compose affected52/5 and fresh backend25 PASS; native10+inbox8+ordinaryP2-D01=19 PASS on LFCrTZzDtX5hNBSRBj5Xx; app/browser TypeScript, lint and build PASS. Actual image review 47/47; all-enabled44scans0violations/330incompletes triaged, human/device/provider qualifications remain. +10front/+0suites,+10browser,+7backend derive739/94,301,1041 only; no integrated full-suite claim. Preserve57/32,RD11/RD21/manual/provider/output/release debt,06Fdeferred/P513withheld/mainunchanged. NextP7-07backend contract first. - [ ] P7-06F optional reviewed followMode composer: retain deferral unless its required action/fallback decision is established. -- [ ] P7-07 owner-scoped metadata projection, then Documents interface. +- [ ] P7-07 [owner-scoped metadata projection](../redesign/phase-7/p7-07/README.md): backend implemented CONDITIONAL GO12new/80related cases+3realcookie native+ordinaryP2-D01PASS; finalhygiene/normalpushpending, actualMariaDBruntimeunverified. DocumentsUI follows verifiedcontractpush. - [ ] P7-08 existing notifications and operations. - [ ] P7-09 remaining Settings, Connections and data lifecycle presentation. - [ ] P7-10 Users, Audit and System administration presentation. @@ -110,3 +110,5 @@ Forms4c325 is pushed. [Inbox/read](../redesign/phase-7/p7-06/INBOX.md) now has42 Final inbox hygiene PASS observed2026-10-14 05:14:23UTC: secret and working/staged/phase whitespace gates pass,15 docs/474 local links resolve,57 groups and32 routes retained. Normal development push pending; main unchanged. Inbox/read committed and normal push verified **a791e353b062cc91e10429cd767350ec9f0aaef2** at2026-10-14T05:14:53.828571UTC: cleanHEAD=origin=live development; local/origin/live main27657f3 unchanged. Second slice implemented CONDITIONAL GO; overallP7-06 ongoing. Mandatory compose/send follows; no production or Phase8 action. + +Compose/send committed and normal push verified **223dcf57d26bab624261994b61a46f2d3e2dd5b2** at 2026-10-14T11:03:15.576853+00:00: clean HEAD = origin = live development; local/origin/live main27657f3 unchanged. All three mandatory P7-06 boundaries are implemented CONDITIONAL GO. P7-07 enters the metadata contract first; production NO-GO and no Phase8 implementation. diff --git a/job-tracker-ui/e2e/phase7-documents-contract.spec.ts b/job-tracker-ui/e2e/phase7-documents-contract.spec.ts new file mode 100644 index 0000000..3ecd7c3 --- /dev/null +++ b/job-tracker-ui/e2e/phase7-documents-contract.spec.ts @@ -0,0 +1,90 @@ +import { expect, test as base, type APIRequestContext, type APIResponse } from '@playwright/test'; + +const apiUrl = 'http://localhost:5302/api'; +const password = 'E2ePassword123!'; +const privateBody = 'DO_NOT_EXPOSE_PRIVATE_DOCUMENT_BODY — æøå\n'; +type Owner = { id: string; headers: Record }; +type Job = { id: number; companyId: number; jobTitle: string }; +type Draft = { id: string; jobApplicationId: number; provider: string; to: string; subject: string; bodyText: string; threadId: string | null; clientRequestId: string; revision: number; createdAtUtc: string; updatedAtUtc: string }; +type Cv = { id: number; name: string; version: number; jobApplicationId: number | null; isPublic: boolean; settings: Record }; +type Item = { id: string; resourceId: string; type: string; name: string; state: string; visibility: string | null; jobApplicationId: number | null; jobTitle: string | null; companyName: string | null; jobIsDeleted: boolean | null; recordedAt: string | null; dateKind: string; version: number | null; historyCount: number | null; attachmentCount: number | null; cvVariantId: number | null; cvVariantName: string | null; cvVariantVersion: number | null; fileType: string | null; fileSize: number | null; purpose: string | null; useForAi: boolean | null; provider: string | null; sourceHref: string; downloadHref: string | null; publicHref: string | null; hasCurrentContent: boolean | null }; +type Documents = { items: Item[]; page: number; pageSize: number; total: number; totalPages: number }; +type ReadReceipt = { actor: string; query: Record; status: number; metadata: Documents | null }; +type Session = { a: Owner; b: Owner; apiA: APIRequestContext; apiB: APIRequestContext; anonymous: APIRequestContext; reads: ReadReceipt[] }; +const fields = ['id', 'resourceId', 'type', 'name', 'state', 'visibility', 'jobApplicationId', 'jobTitle', 'companyName', 'jobIsDeleted', 'recordedAt', 'dateKind', 'version', 'historyCount', 'attachmentCount', 'cvVariantId', 'cvVariantName', 'cvVariantVersion', 'fileType', 'fileSize', 'purpose', 'useForAi', 'provider', 'sourceHref', 'downloadHref', 'publicHref', 'hasCurrentContent'].sort(); +const boundary = 'Three API-only native Playwright cases run the actual Program, disposable SQLite, real local Identity login/session cookies and CSRF-protected source writes at localhost5302. Each case creates independent free A/B accounts through the existing admin/users API. The prebuilt UI server on3300 is only the existing launch fixture; no Documents UI, browser crawl, render, PDF, download, storage relocation, provider grant, send, Gmail operation or model request is executed. Metadata response receipts retain only the exact27 public keys and fixture metadata; no authentication/session/CSRF headers or source bodies. Repeated GET and independent source readback prove observable metadata/persistence, not internal service-call or SQL-command counts. No-generation dependencies, bounded database LIMIT/count and all seven resource kinds remain separately qualified by backend contract tests. These cookie-authentication cases supplement those tests, which use a controlled test identity. No schema/raw SQL/production/deployment action. Browser network guards block external origins and delivery/model endpoints if accidentally requested; all API request destinations are the fixed local API. API-only evidence does not establish migrated UI, responsive, visual, axe, human reader/device, production ingress or release readiness.'; +async function json(response: APIResponse, status = 200): Promise { expect(response.status(), response.url()).toBe(status); return response.json(); } +async function login(api: APIRequestContext, email: string): Promise { + expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); + const me = await json<{ id: string; sessionContext: string }>(await api.get(`${apiUrl}/auth/me`)); + const token = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN'); expect(token).toBeDefined(); + return { id: me.id, headers: { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext } }; +} +async function account(admin: APIRequestContext, owner: Owner, actor: string) { + const email = `p707-documents-${actor}-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: owner.headers, data: { email, password, roles: [] } })).status()).toBe(200); return email; +} +const test = base.extend<{ session: Session }>({ + session: async ({ page, context, playwright }, provide, info) => { + expect(process.env.PLAYWRIGHT_REUSE_SERVERS).not.toBe('true'); expect(info.config.metadata.isolatedDataRoot).toEqual(expect.any(String)); + const admin = await playwright.request.newContext(); const apiB = await playwright.request.newContext(); const anonymous = await playwright.request.newContext(); const reads: ReadReceipt[] = []; const blocked: string[] = []; const browserRequests: string[] = []; + page.on('request', request => browserRequests.push(`${request.method()} ${new URL(request.url()).origin}${new URL(request.url()).pathname}`)); + await context.route('**/*', route => { const url = new URL(route.request().url()); if (/^\/api\/(?:email\/send|gmail|ai)(?:\/|$)/.test(url.pathname) || !['localhost', '127.0.0.1'].includes(url.hostname) && !['blob:', 'data:', 'about:'].includes(url.protocol)) { blocked.push(url.origin + url.pathname); return route.abort(); } return route.continue(); }); + try { + const administrator = await login(admin, 'e2e@example.test'); const a = await login(page.request, await account(admin, administrator, 'a')); const b = await login(apiB, await account(admin, administrator, 'b')); + for (const api of [page.request, apiB]) expect((await json<{ entitlements: { ai: boolean } }>(await api.get(`${apiUrl}/auth/me`))).entitlements.ai).toBe(false); + expect(a.id).not.toBe(b.id); await provide({ a, b, apiA: page.request, apiB, anonymous, reads }); expect(browserRequests).toEqual([]); expect(blocked).toEqual([]); + } finally { await info.attach('documents-real-cookie-contract-boundary', { body: JSON.stringify({ boundary, reads, browserRequests, blockedExternalOrDeliveryRequests: blocked }), contentType: 'application/json' }); await admin.dispose(); await apiB.dispose(); await anonymous.dispose(); } + }, +}); +async function documents(session: Session, query: Record = {}, actor: 'a' | 'b' | 'anonymous' = 'a', expectedStatus = 200) { + const api = actor === 'a' ? session.apiA : actor === 'b' ? session.apiB : session.anonymous; const response = await api.get(`${apiUrl}/documents`, { params: query }); const status = response.status(); const data = status === 200 ? await response.json() as Documents : null; + session.reads.push({ actor, query, status, metadata: data }); expect(status, response.url()).toBe(expectedStatus); if (!data) return null; + expect(Object.keys(data).sort()).toEqual(['items', 'page', 'pageSize', 'total', 'totalPages'].sort()); expect(data.items.length).toBeLessThanOrEqual(data.pageSize); expect(data.totalPages).toBe(Math.ceil(data.total / data.pageSize)); + for (const item of data.items) { expect(Object.keys(item).sort()).toEqual(fields); expect(item.id).toBe(`${item.type}:${item.resourceId}`); expect(item.sourceHref).toMatch(/^\//); } + expect(JSON.stringify(data)).not.toContain('DO_NOT_EXPOSE_PRIVATE_DOCUMENT_BODY'); expect(JSON.stringify(data)).not.toContain('private-recipient@example.test'); expect(JSON.stringify(data)).not.toContain('private-thread-fixture'); return data; +} +async function job(api: APIRequestContext, owner: Owner, title: string, ownedCompanyId?: number) { + const companyId = ownedCompanyId ?? (await json<{ id: number }>(await api.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Recorded metadata company' } }), 201)).id; + return json(await api.post(`${apiUrl}/jobapplications`, { headers: owner.headers, data: { companyId, jobTitle: title, status: 'Saved', notes: privateBody } }), 201); +} +async function email(api: APIRequestContext, owner: Owner, parent: Job, subject: string) { + return json(await api.post(`${apiUrl}/email/drafts`, { headers: owner.headers, data: { jobApplicationId: parent.id, provider: 'gmail', to: 'private-recipient@example.test', subject, bodyText: privateBody, threadId: 'private-thread-fixture' } }), 201); +} +async function cv(api: APIRequestContext, owner: Owner, name: string, jobApplicationId: number | null = null) { + return json(await api.post(`${apiUrl}/cv/variants`, { headers: owner.headers, data: { name, jobApplicationId, settings: { themeId: 'minimal', language: 'nb-NO', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [] } } })); +} + +test('[P7-07 documents contract] actual owner metadata has exact27 keys, literal Nordic search, intersecting filters and stable paginated rereads', async ({ session }) => { + test.setTimeout(120_000); const empty = await documents(session); expect(empty).toEqual({ items: [], page: 1, pageSize: 25, total: 0, totalPages: 0 }); + const firstJob = await job(session.apiA, session.a, 'Recorded document role'); const secondJob = await job(session.apiA, session.a, 'Second document role', firstJob.companyId); + const literal = await email(session.apiA, session.a, firstJob, 'Literal 100%_! ÆØÅ Nordisk'); const wildcardControl = await email(session.apiA, session.a, firstJob, 'Literal 100ABX Nordisk'); const other = await email(session.apiA, session.a, secondJob, 'Other application notes Åse'); const variant = await cv(session.apiA, session.a, 'Metadata CV Åse'); + const all = await documents(session, { sort: 'name-asc' }); expect(all).toMatchObject({ page: 1, pageSize: 25, total: 4, totalPages: 1 }); expect(all!.items.map(item => item.id)).toEqual([`email-draft:${literal.id}`, `email-draft:${wildcardControl.id}`, `cv:${variant.id}`, `email-draft:${other.id}`]); + const projected = all!.items.find(item => item.id === `email-draft:${literal.id}`)!; expect(projected).toMatchObject({ resourceId: literal.id, name: literal.subject, type: 'email-draft', state: 'draft', visibility: null, jobApplicationId: firstJob.id, jobTitle: firstJob.jobTitle, companyName: 'Recorded metadata company', provider: 'gmail', version: 1, dateKind: 'updated-utc', sourceHref: '/correspondence', downloadHref: null, publicHref: null, hasCurrentContent: null }); expect(projected.recordedAt).toMatch(/Z$/); expect(JSON.stringify(all)).not.toContain(literal.clientRequestId); + for (const q of ['%_!', '%', '_', '!', 'æøå']) { const matched = await documents(session, { q, sort: 'name-asc' }); expect(matched!.total).toBe(1); expect(matched!.items.map(item => item.id)).toEqual([projected.id]); } + expect((await documents(session, { q: 'DO_NOT_EXPOSE_PRIVATE_DOCUMENT_BODY' }))!.total).toBe(0); + const first = await documents(session, { sort: 'name-asc', pageSize: 2 }); const second = await documents(session, { sort: 'name-asc', pageSize: 2, page: 2 }); expect(first).toMatchObject({ page: 1, pageSize: 2, total: 4, totalPages: 2 }); expect(second).toMatchObject({ page: 2, pageSize: 2, total: 4, totalPages: 2 }); expect([...first!.items, ...second!.items]).toEqual(all!.items); expect(await documents(session, { sort: 'name-asc', pageSize: 2 })).toEqual(first); + const filtered = await documents(session, { type: 'email-draft', state: 'draft', jobApplicationId: firstJob.id }); expect(filtered!.total).toBe(2); expect(filtered!.items.map(item => item.id).sort()).toEqual([`email-draft:${literal.id}`, `email-draft:${wildcardControl.id}`].sort()); + const cvs = await documents(session, { type: 'cv', state: 'current', visibility: 'private' }); expect(cvs!.items.map(item => item.id)).toEqual([`cv:${variant.id}`]); expect((await documents(session, { visibility: 'public' }))!.total).toBe(0); expect((await documents(session, { type: 'submitted-package' }))!.total).toBe(0); + expect(await json(await session.apiA.get(`${apiUrl}/email/drafts/${literal.id}`))).toEqual(literal); expect(await json(await session.apiA.get(`${apiUrl}/cv/variants/${variant.id}`))).toEqual(variant); +}); + +test('[P7-07 documents contract] actual cookies require authentication and isolate owner metadata despite forged owner or foreign-parent filters', async ({ session }) => { + test.setTimeout(120_000); const jobA = await job(session.apiA, session.a, 'Private owner A role'); const jobB = await job(session.apiB, session.b, 'Private owner B role'); const draftA = await email(session.apiA, session.a, jobA, 'Private owner A subject'); const draftB = await email(session.apiB, session.b, jobB, 'Private owner B subject'); + await documents(session, {}, 'anonymous', 401); const a = await documents(session); const b = await documents(session, {}, 'b'); expect(a!.items.map(item => item.id)).toEqual([`email-draft:${draftA.id}`]); expect(b!.items.map(item => item.id)).toEqual([`email-draft:${draftB.id}`]); + expect(await documents(session, { ownerUserId: session.b.id })).toEqual(a); expect(await documents(session, { ownerUserId: session.a.id }, 'b')).toEqual(b); const foreign = await documents(session, { jobApplicationId: jobA.id }, 'b'); expect(foreign).toMatchObject({ items: [], total: 0, totalPages: 0 }); expect((await documents(session, { jobApplicationId: jobB.id }))!.items).toEqual([]); + expect((await session.apiB.get(`${apiUrl}/email/drafts/${draftA.id}`)).status()).toBe(404); expect((await session.apiA.get(`${apiUrl}/email/drafts/${draftB.id}`)).status()).toBe(404); expect((await session.anonymous.get(`${apiUrl}/email/drafts/${draftA.id}`)).status()).toBe(401); + expect((await session.apiB.post(`${apiUrl}/email/drafts`, { headers: session.b.headers, data: { jobApplicationId: jobA.id, provider: 'gmail', to: '', subject: 'Forbidden association', bodyText: '' } })).status()).toBe(404); expect(await documents(session)).toEqual(a); expect(await documents(session, {}, 'b')).toEqual(b); expect(await json(await session.apiA.get(`${apiUrl}/email/drafts/${draftA.id}`))).toEqual(draftA); +}); + +test('[P7-07 documents contract] existing CV and email revisions detach and deletes preserve projection identities without metadata-list writes', async ({ session }) => { + test.setTimeout(150_000); const parent = await job(session.apiA, session.a, 'Document source mutations'); const originalCv = await cv(session.apiA, session.a, 'Original metadata CV', parent.id); const originalEmail = await email(session.apiA, session.a, parent, 'Original metadata email'); const initial = await documents(session, { sort: 'name-asc' }); expect(initial!.items.map(item => item.id).sort()).toEqual([`cv:${originalCv.id}`, `email-draft:${originalEmail.id}`].sort()); + const invalid: Record[] = [{ page: 0 }, { pageSize: 101 }, { type: 'unknown' }, { state: 'sent' }, { visibility: 'shared' }, { sort: 'date-desc;DROP TABLE CvVariants' }, { q: 'a'.repeat(201) }, { jobApplicationId: 0 }]; for (const query of invalid) await documents(session, query, 'a', 400); + expect(await json(await session.apiA.get(`${apiUrl}/cv/variants/${originalCv.id}`))).toEqual(originalCv); expect(await json(await session.apiA.get(`${apiUrl}/email/drafts/${originalEmail.id}`))).toEqual(originalEmail); expect(await documents(session, { sort: 'name-asc' })).toEqual(initial); + const revisedCv = await json(await session.apiA.put(`${apiUrl}/cv/variants/${originalCv.id}`, { headers: session.a.headers, data: { name: 'Revised metadata CV ÆØÅ', settings: originalCv.settings, expectedVersion: originalCv.version, source: 'manual' } })); expect(revisedCv).toMatchObject({ id: originalCv.id, name: 'Revised metadata CV ÆØÅ', version: 2, jobApplicationId: parent.id }); + const revisedEmail = await json(await session.apiA.put(`${apiUrl}/email/drafts/${originalEmail.id}`, { headers: session.a.headers, data: { revision: originalEmail.revision, to: originalEmail.to, subject: 'Revised metadata email ÆØÅ', bodyText: `${privateBody}\nNew owner-only body` } })); expect(revisedEmail).toMatchObject({ id: originalEmail.id, clientRequestId: originalEmail.clientRequestId, revision: 2 }); + const revised = await documents(session, { sort: 'name-asc' }); expect(revised!.total).toBe(2); expect(revised!.items.map(item => item.id).sort()).toEqual(initial!.items.map(item => item.id).sort()); expect(revised!.items.find(item => item.type === 'cv')).toMatchObject({ name: revisedCv.name, version: 2, jobApplicationId: parent.id }); expect(revised!.items.find(item => item.type === 'email-draft')).toMatchObject({ name: revisedEmail.subject, version: 2, jobApplicationId: parent.id }); expect(JSON.stringify(revised)).not.toContain(revisedEmail.bodyText); expect(JSON.stringify(revised)).not.toContain(revisedEmail.clientRequestId); + await json(await session.apiA.put(`${apiUrl}/jobapplications/${parent.id}/cv`, { headers: session.a.headers, data: { variantId: null } })); const detached = await documents(session); expect(detached!.items.find(item => item.type === 'cv')).toMatchObject({ id: `cv:${originalCv.id}`, resourceId: String(originalCv.id), name: revisedCv.name, version: 2, jobApplicationId: null, jobTitle: null, companyName: null }); expect((await documents(session, { jobApplicationId: parent.id }))!.items.map(item => item.id)).toEqual([`email-draft:${originalEmail.id}`]); + expect((await session.apiA.delete(`${apiUrl}/email/drafts/${originalEmail.id}`, { headers: session.a.headers, params: { revision: revisedEmail.revision } })).status()).toBe(204); const afterEmailDelete = await documents(session); expect(afterEmailDelete!.total).toBe(1); expect(afterEmailDelete!.items.map(item => item.id)).toEqual([`cv:${originalCv.id}`]); expect((await session.apiA.get(`${apiUrl}/email/drafts/${originalEmail.id}`)).status()).toBe(404); + expect((await session.apiA.delete(`${apiUrl}/cv/variants/${originalCv.id}`, { headers: session.a.headers })).status()).toBe(204); expect(await documents(session)).toEqual({ items: [], page: 1, pageSize: 25, total: 0, totalPages: 0 }); expect((await session.apiA.get(`${apiUrl}/cv/variants/${originalCv.id}`)).status()).toBe(404); +});