feat(cv-import): add source-grounded review and explicit corrections
This commit is contained in:
@@ -42,11 +42,17 @@ public sealed partial class ProfileCvController
|
||||
var candidate = CvDocumentInterpreter.Parse(blocks);
|
||||
await Stage("cv_validating_details");
|
||||
var profile = CvDocumentReview.Ground(candidate, document);
|
||||
foreach (var field in profile.Metadata.Fields.Values)
|
||||
{
|
||||
field.SourceArtifactId = artifact.Id;
|
||||
field.SourceExtractionRunId = run.Id;
|
||||
field.SourceFileSha256 = artifact.Sha256;
|
||||
}
|
||||
await Stage("cv_preparing_review");
|
||||
await Stage("cv_review_ready");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var documentJson = JsonSerializer.Serialize(document);
|
||||
var profileJson = StructuredCvProfileJson.Serialize(profile);
|
||||
var profileJson = StructuredCvProfileJson.SerializePersisted(profile);
|
||||
if (context is null)
|
||||
{
|
||||
run.DocumentJson = documentJson;
|
||||
@@ -54,8 +60,9 @@ public sealed partial class ProfileCvController
|
||||
return;
|
||||
}
|
||||
var currentLease = context.Lease;
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var published = await _db.CvExtractionRuns.Where(r => r.Id == run.Id && r.OwnerUserId == run.OwnerUserId && r.Status == "running" && _db.UserOperations.Any(o => o.Id == currentLease.OperationId && o.LeaseToken == currentLease.LeaseToken && o.Status == OperationStatuses.Running && o.CancellationRequestedAtUtc == null && o.LeaseExpiresAtUtc > DateTime.UtcNow))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(r => r.DocumentJson, documentJson).SetProperty(r => r.RawExtractedText, document.Text).SetProperty(r => r.NormalizedText, document.Text).SetProperty(r => r.StructuredProfileJson, profileJson).SetProperty(r => r.Status, "pending_review").SetProperty(r => r.CompletedAtUtc, DateTimeOffset.UtcNow), cancellationToken);
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(r => r.DocumentJson, documentJson).SetProperty(r => r.RawExtractedText, document.Text).SetProperty(r => r.NormalizedText, document.Text).SetProperty(r => r.StructuredProfileJson, profileJson).SetProperty(r => r.Status, "pending_review").SetProperty(r => r.CompletedAtUtc, completedAt), cancellationToken);
|
||||
if (published != 1) throw new OperationCanceledException("The document processing lease was lost before publication.", cancellationToken);
|
||||
await _db.Entry(run).ReloadAsync(cancellationToken);
|
||||
await PruneExtractionRunsAsync(run.OwnerUserId, cancellationToken);
|
||||
@@ -126,7 +133,8 @@ public sealed partial class ProfileCvController
|
||||
using var reader = new StreamReader(entry.Open());
|
||||
var relationships = await reader.ReadToEndAsync(cancellationToken);
|
||||
// Hyperlinks remain inert text; linked templates/OLE/media are refused.
|
||||
var xml = System.Xml.Linq.XDocument.Parse(relationships);
|
||||
using var xmlReader = System.Xml.XmlReader.Create(new StringReader(relationships), new System.Xml.XmlReaderSettings { DtdProcessing = System.Xml.DtdProcessing.Prohibit, XmlResolver = null, MaxCharactersInDocument = 2 * 1024 * 1024 });
|
||||
var xml = System.Xml.Linq.XDocument.Load(xmlReader);
|
||||
if (xml.Descendants().Any(e => string.Equals((string?)e.Attribute("TargetMode"), "External", StringComparison.OrdinalIgnoreCase) && !((string?)e.Attribute("Type") ?? "").EndsWith("/hyperlink", StringComparison.Ordinal))) throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
@@ -151,7 +159,7 @@ public sealed partial class ProfileCvController
|
||||
if (run.ParserVersion != DocumentParserVersion || string.IsNullOrWhiteSpace(run.StructuredProfileJson)) return Conflict("This import is not ready for review.");
|
||||
var snapshot = await _careerProfileService.LoadSnapshotAsync(user.Id, HttpContext.RequestAborted);
|
||||
var document = JsonSerializer.Deserialize<CvImportDocument>(run.DocumentJson ?? "null");
|
||||
return Ok(new CvImportReview(run.Id, run.Status, snapshot.Revision, run.Artifact?.OriginalFileName, $"/api/profile-cv/runs/{run.Id}/source", run.RawExtractedText, document?.Warnings ?? Array.Empty<string>(), CvDocumentReview.Build(snapshot.Profile, StructuredCvProfileJson.Deserialize(run.StructuredProfileJson))));
|
||||
return Ok(new CvImportReview(run.Id, run.Status, snapshot.Revision, run.Artifact?.OriginalFileName, $"/api/profile-cv/runs/{run.Id}/source", run.RawExtractedText, document?.Warnings ?? Array.Empty<string>(), CvDocumentReview.Build(snapshot.Profile, StructuredCvProfileJson.DeserializePersisted(run.StructuredProfileJson))));
|
||||
}
|
||||
|
||||
[HttpGet("runs/{id:int}/source")]
|
||||
@@ -189,11 +197,11 @@ public sealed partial class ProfileCvController
|
||||
if (run.Status != "pending_review" || string.IsNullOrWhiteSpace(run.StructuredProfileJson)) return Conflict("This import is not awaiting review.");
|
||||
var snapshot = await _careerProfileService.LoadSnapshotAsync(user.Id, HttpContext.RequestAborted);
|
||||
if (snapshot.Revision != request.ExpectedVersion) throw new DbUpdateConcurrencyException();
|
||||
var merged = CvDocumentReview.Apply(snapshot.Profile, StructuredCvProfileJson.Deserialize(run.StructuredProfileJson), request.Decisions);
|
||||
var merged = CvDocumentReview.Apply(snapshot.Profile, StructuredCvProfileJson.DeserializePersisted(run.StructuredProfileJson), request.Decisions);
|
||||
if (!request.Decisions.Any(d => d.Action != "keep")) return BadRequest("No imported fields were selected. Discard this import to keep your current profile.");
|
||||
var validation = CareerProfileValidator.Validate(merged);
|
||||
if (validation is not null || StructuredCvProfileJson.SerializePersisted(merged).Length > 500000) return BadRequest(validation ?? "The reviewed profile exceeds its size limit.");
|
||||
merged.Metadata.ProfileVersion = (user.CurrentCvProfileVersion ?? 0) + 1;
|
||||
merged.Metadata.ProfileVersion = snapshot.Revision + 1;
|
||||
merged.Metadata.AppliedExtractionRunId = run.Id;
|
||||
merged.Metadata.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _careerProfileService.SaveVersionAsync(user.Id, merged, "document_import:reviewed", request.ExpectedVersion.Value, HttpContext.RequestAborted);
|
||||
|
||||
Reference in New Issue
Block a user