From 99e7da0d210628b8bfdf070634e7ec96d99ea9fc Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Tue, 17 Mar 2026 22:03:09 +0100 Subject: [PATCH] Add critical result curation metadata --- docs/critical_tables_db_model.md | 18 +++++++- docs/critical_tables_schema.sql | 5 ++- .../Data/RolemasterDbContext.cs | 1 + .../Data/RolemasterDbSchemaUpgrader.cs | 44 +++++++++++++++++++ src/RolemasterDb.App/Domain/CriticalResult.cs | 4 ++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/critical_tables_db_model.md b/docs/critical_tables_db_model.md index 70e2f14..2739c33 100644 --- a/docs/critical_tables_db_model.md +++ b/docs/critical_tables_db_model.md @@ -102,11 +102,23 @@ One record per lookup cell: This stores: +- `is_curated` - `raw_cell_text` - `description_text` - `raw_affix_text` - `parsed_json` -- parse status / source metadata +- `parse_status` +- `source_page_number` +- `source_image_path` +- `source_image_crop` + +`is_curated` is an explicit workflow flag. Once a result is curated in the web editor, later importer runs must preserve curator-owned content instead of replacing the row wholesale. + +The source-image fields keep importer provenance separate from the editor snapshot stored in `parsed_json`: + +- `source_page_number` points to the rendered PDF page used for review +- `source_image_path` stores the importer-managed relative PNG path for the cell crop +- `source_image_crop` stores the crop geometry that produced the PNG and can be used for debugging alignment problems ### 6. `critical_branch` @@ -284,6 +296,7 @@ Current curation flow: - base raw cell text - curated prose / description - raw affix text + - curated state - parse status - parsed JSON - nested `critical_branch` rows @@ -293,6 +306,7 @@ Current curation flow: The corresponding API endpoints are: - `GET /api/tables/critical/{slug}/cells/{resultId}` +- `GET /api/tables/critical/{slug}/cells/{resultId}/source-image` - `PUT /api/tables/critical/{slug}/cells/{resultId}` -The save operation replaces the stored branches and effects for that cell with the submitted payload. That keeps manual edits deterministic and avoids trying to reconcile partial child-row diffs against importer-generated data. +The save operation replaces the stored branches and effects for that cell with the submitted payload and updates the explicit curated flag. Importer-managed source provenance can still be refreshed on later imports without overwriting curated content. diff --git a/docs/critical_tables_schema.sql b/docs/critical_tables_schema.sql index fae90ab..19bd592 100644 --- a/docs/critical_tables_schema.sql +++ b/docs/critical_tables_schema.sql @@ -52,12 +52,15 @@ create table critical_result ( critical_group_id bigint references critical_group(id) on delete cascade, critical_column_id bigint not null references critical_column(id) on delete cascade, critical_roll_band_id bigint not null references critical_roll_band(id) on delete cascade, + is_curated boolean not null default false, raw_cell_text text not null, description_text text, raw_affix_text text, parsed_json jsonb not null default '{}'::jsonb, parse_status text not null default 'raw' check (parse_status in ('raw', 'partial', 'parsed', 'verified')), - source_bbox jsonb, + source_page_number integer, + source_image_path text, + source_image_crop jsonb, created_at timestamptz not null default now() ); diff --git a/src/RolemasterDb.App/Data/RolemasterDbContext.cs b/src/RolemasterDb.App/Data/RolemasterDbContext.cs index be730aa..5962c32 100644 --- a/src/RolemasterDb.App/Data/RolemasterDbContext.cs +++ b/src/RolemasterDb.App/Data/RolemasterDbContext.cs @@ -79,6 +79,7 @@ public sealed class RolemasterDbContext(DbContextOptions op { entity.HasIndex(item => new { item.CriticalTableId, item.CriticalGroupId, item.CriticalColumnId, item.CriticalRollBandId }).IsUnique(); entity.Property(item => item.ParseStatus).HasMaxLength(32); + entity.Property(item => item.SourceImagePath).HasMaxLength(512); }); modelBuilder.Entity(entity => diff --git a/src/RolemasterDb.App/Data/RolemasterDbSchemaUpgrader.cs b/src/RolemasterDb.App/Data/RolemasterDbSchemaUpgrader.cs index 096de14..6dc400d 100644 --- a/src/RolemasterDb.App/Data/RolemasterDbSchemaUpgrader.cs +++ b/src/RolemasterDb.App/Data/RolemasterDbSchemaUpgrader.cs @@ -8,6 +8,7 @@ public static class RolemasterDbSchemaUpgrader public static async Task EnsureLatestAsync(RolemasterDbContext dbContext, CancellationToken cancellationToken = default) { await EnsureAttackTableFumbleColumnsAsync(dbContext, cancellationToken); + await EnsureCriticalResultCurationColumnsAsync(dbContext, cancellationToken); await dbContext.Database.ExecuteSqlRawAsync( """ @@ -91,6 +92,49 @@ public static class RolemasterDbSchemaUpgrader cancellationToken); } + private static async Task EnsureCriticalResultCurationColumnsAsync(RolemasterDbContext dbContext, CancellationToken cancellationToken) + { + if (!await ColumnExistsAsync(dbContext, "CriticalResults", "IsCurated", cancellationToken)) + { + await dbContext.Database.ExecuteSqlRawAsync( + """ + ALTER TABLE "CriticalResults" + ADD COLUMN "IsCurated" INTEGER NOT NULL DEFAULT 0; + """, + cancellationToken); + } + + if (!await ColumnExistsAsync(dbContext, "CriticalResults", "SourcePageNumber", cancellationToken)) + { + await dbContext.Database.ExecuteSqlRawAsync( + """ + ALTER TABLE "CriticalResults" + ADD COLUMN "SourcePageNumber" INTEGER NULL; + """, + cancellationToken); + } + + if (!await ColumnExistsAsync(dbContext, "CriticalResults", "SourceImagePath", cancellationToken)) + { + await dbContext.Database.ExecuteSqlRawAsync( + """ + ALTER TABLE "CriticalResults" + ADD COLUMN "SourceImagePath" TEXT NULL; + """, + cancellationToken); + } + + if (!await ColumnExistsAsync(dbContext, "CriticalResults", "SourceImageCropJson", cancellationToken)) + { + await dbContext.Database.ExecuteSqlRawAsync( + """ + ALTER TABLE "CriticalResults" + ADD COLUMN "SourceImageCropJson" TEXT NULL; + """, + cancellationToken); + } + } + private static async Task EnsureAttackTableFumbleColumnsAsync(RolemasterDbContext dbContext, CancellationToken cancellationToken) { if (!await ColumnExistsAsync(dbContext, "AttackTables", "FumbleMinRoll", cancellationToken)) diff --git a/src/RolemasterDb.App/Domain/CriticalResult.cs b/src/RolemasterDb.App/Domain/CriticalResult.cs index 1bcb3ec..c858a08 100644 --- a/src/RolemasterDb.App/Domain/CriticalResult.cs +++ b/src/RolemasterDb.App/Domain/CriticalResult.cs @@ -7,11 +7,15 @@ public sealed class CriticalResult public int? CriticalGroupId { get; set; } public int CriticalColumnId { get; set; } public int CriticalRollBandId { get; set; } + public bool IsCurated { get; set; } public string RawCellText { get; set; } = string.Empty; public string DescriptionText { get; set; } = string.Empty; public string? RawAffixText { get; set; } public string ParsedJson { get; set; } = "{}"; public string ParseStatus { get; set; } = "verified"; + public int? SourcePageNumber { get; set; } + public string? SourceImagePath { get; set; } + public string? SourceImageCropJson { get; set; } public CriticalTable CriticalTable { get; set; } = null!; public CriticalGroup? CriticalGroup { get; set; } public CriticalColumn CriticalColumn { get; set; } = null!;