Skip to content

Commit 18a4fce

Browse files
munessclaude
andcommitted
fix: address CodeRabbit findings from PR #132
- AdminEndpoints: scope reprocess DELETE to documents being reset, not all tenant data - ReviewEndpoints finalize: merge discovered fields into case_profiles search_text - ExtractionBackgroundService: persist extraction_attempts for discovered fields (audit trail) - ExtractionPipelineService: sort discoveredResults by FieldKey for stable serialization order - Worker.Testing.cs: add RunExtractionPipelineWithDiscoveredForTests exposing full tuple - CaseProfileText.BuildFinalized: accept optional discoveredFields parameter - ReviewerDashboard: show confidence badge on discovered fields - DevPage: refresh queue state after reprocess succeeds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bb213a5 commit 18a4fce

9 files changed

Lines changed: 121 additions & 16 deletions

File tree

.oh/sessions/131-dev.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,57 @@
1818
- Admin/dev endpoint or migration to requeue existing documents for reprocessing
1919

2020
## Phase 2: Solution Space
21-
(filled by Phase 2)
21+
22+
**Selected approach:** Option C — open-ended prompt + schema overlay.
23+
24+
- Change vision prompt to "extract ALL fields + schema fields"
25+
- Provider marks discovered keys with `is_discovered=true` in metadata
26+
- `ExtractionPipelineService.RunExtractionPipeline` returns `(schemaFields, discoveredFields)` tuple
27+
- Discovered fields stored with `is_discovered = true`, `requires_review = false`, don't affect document status
28+
- `interviewDate` added to all 4 template schemas via M006 migration + SeedTemplatesSql update
29+
- Frontend shows discovered fields in collapsible "Additional fields" section (read-only with confidence badge)
30+
- `POST /admin/reprocess` endpoint resets documents to `uploaded` status scoped to eligible docs only
2231

2332
## Phase 3: Execute
24-
(filled by Phase 3)
33+
34+
**Files changed:**
35+
- `DatabaseInitializer.cs` — M005 (is_discovered column) + M006 (interviewDate migration) + SeedTemplatesSql (8 templates updated)
36+
- `OpenAiVisionProvider.cs` — open-ended prompt, emits discovered fields with is_discovered=true in metadata
37+
- `ExtractionPipelineService.cs` — returns (schemaFields, discoveredFields) tuple, sorts discovered fields by key
38+
- `ExtractionBackgroundService.cs` — persists schema and discovered fields separately, includes discovered in case profile
39+
- `CaseProfileService.cs` — BuildCaseProfileText accepts optional discoveredFields
40+
- `CaseProfileText.cs` (contracts) — BuildFinalized accepts optional discoveredFields, preserved during finalization
41+
- `Models.cs` (contracts) — ReviewField gains IsDiscovered = false default parameter
42+
- `ReviewEndpoints.cs` — queries is_discovered, passes discovered fields to finalize search text
43+
- `AdminEndpoints.cs` — POST /admin/reprocess scoped to eligible documents only
44+
- `Worker.Testing.cs` — RunExtractionPipelineForTests returns schema only; new RunExtractionPipelineWithDiscoveredForTests returns both
45+
- `ReviewerDashboard.tsx` — filters discovered from editable fields, shows collapsible "Additional fields" with confidence badges
46+
- `DevPage.tsx` — Admin section with reprocess button (Admin role gate, clears state on success)
47+
- `api.ts` — reprocessDocuments method added
48+
- `types.ts` — ReviewField gains optional isDiscovered
49+
50+
**Build status:** Clean (0 warnings, 0 errors)
51+
**Tests:** 29/29 worker unit tests pass, 4/4 tenancy unit tests pass
2552

2653
## Phase 4: Ship
27-
(filled by Phase 4)
54+
55+
**PR:** #132
56+
**Review findings addressed:**
57+
- Unreachable `"true"` string branch removed from ExtractionPipelineService
58+
- DevPage reprocess button disabled for non-Admin users
59+
**CodeRabbit findings addressed:**
60+
- AdminEndpoints reprocess: scoped DELETE to documents being reset (not all tenant data)
61+
- ReviewEndpoints finalize: discovered fields now merged into case profile search text
62+
- ExtractionBackgroundService: discovered fields now also persisted to extraction_attempts
63+
- ExtractionPipelineService: discovered results sorted by FieldKey for stable ordering
64+
- Worker.Testing.cs: added RunExtractionPipelineWithDiscoveredForTests exposing full tuple
65+
- ReviewerDashboard: discovered fields now show confidence badge
66+
- DevPage: reprocess refreshes queue state after success
67+
68+
**Delivery verification:**
69+
- `pnpm check` passes
70+
- All unit tests pass
71+
- Post-reprocess: documents with interviewDate on form will show it in extracted fields after re-run
2872

2973
## RNA Tool Friction Log
3074
<!-- Append entries as you encounter friction with RNA MCP tools. -->

apps/web/src/pages/DevPage.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,12 @@ export default function DevPage() {
13411341
try {
13421342
const result = await api.reprocessDocuments(auth.accessToken)
13431343
setReprocessResult(`Queued ${result.queued} document(s) for reprocessing (tenant: ${result.tenantId}).`)
1344+
// Clear stale state and refresh so the UI reflects the backend reset
1345+
setSelectedReviewId(null)
1346+
setReviewDetail(null)
1347+
setIntakeStatus(null)
1348+
setActiveIntakeId(null)
1349+
await refreshQueue(auth.accessToken)
13441350
} catch (err) {
13451351
setReprocessError(userMessage(err, 'Reprocess failed.'))
13461352
} finally {

apps/web/src/pages/ReviewerDashboard.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,12 +1056,23 @@ function ReviewDetail({
10561056
</button>
10571057
{discoveredOpen ? (
10581058
<dl className="mt-2 space-y-1.5">
1059-
{discoveredFields.map((f) => (
1060-
<div key={f.fieldKey} className="rounded border border-slate-200 bg-slate-50 px-3 py-2">
1061-
<dt className="text-xs font-medium text-slate-600">{f.fieldKey}</dt>
1062-
<dd className="mt-0.5 text-xs text-slate-800 break-words">{f.value || '—'}</dd>
1063-
</div>
1064-
))}
1059+
{discoveredFields.map((f) => {
1060+
const tier = confidenceTone(f.confidence)
1061+
return (
1062+
<div key={f.fieldKey} className="rounded border border-slate-200 bg-slate-50 px-3 py-2">
1063+
<div className="flex items-center justify-between gap-2">
1064+
<dt className="text-xs font-medium text-slate-600">{f.fieldKey}</dt>
1065+
<span
1066+
className={`shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium ${tier.badgeClass}`}
1067+
aria-label={`${confidencePercent(f.confidence)} confidence`}
1068+
>
1069+
{confidencePercent(f.confidence)}
1070+
</span>
1071+
</div>
1072+
<dd className="mt-0.5 text-xs text-slate-800 break-words">{f.value || '—'}</dd>
1073+
</div>
1074+
)
1075+
})}
10651076
</dl>
10661077
) : null}
10671078
</div>

src/BuildingBlocks/Northwoods.Contracts/CaseProfileText.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ namespace Northwoods.Contracts;
1010
public static class CaseProfileText
1111
{
1212
/// <summary>
13-
/// Builds search text incorporating finalized field values, OCR segments, and reviewer note.
14-
/// Used when a reviewer finalizes a document so the case profile reflects corrections.
13+
/// Builds search text incorporating finalized field values, OCR segments, reviewer note,
14+
/// and any discovered (non-schema) fields so they remain searchable after finalization.
1515
/// </summary>
1616
public static string BuildFinalized(
1717
string templateId,
1818
IReadOnlyList<ConfidenceField> fields,
1919
IEnumerable<(string FieldKey, string RawValue)> ocrSegments,
20-
string? reviewerNote)
20+
string? reviewerNote,
21+
IEnumerable<(string FieldKey, string Value)>? discoveredFields = null)
2122
{
2223
var fieldPairs = fields
2324
.Select(f => $"{f.FieldKey}: {f.Value}")
@@ -38,6 +39,17 @@ public static string BuildFinalized(
3839
if (!string.IsNullOrWhiteSpace(reviewerNote))
3940
result += $"; reviewer_note={reviewerNote}";
4041

42+
if (discoveredFields is not null)
43+
{
44+
var discoveredPairs = discoveredFields
45+
.Select(d => $"{d.FieldKey}: {d.Value}")
46+
.Where(v => !string.IsNullOrWhiteSpace(v))
47+
.ToArray();
48+
49+
if (discoveredPairs.Length > 0)
50+
result += $"; discovered={string.Join(" | ", discoveredPairs)}";
51+
}
52+
4153
return result;
4254
}
4355
}

src/Services/Northwoods.Api/Endpoints/AdminEndpoints.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,33 @@ del_docs AS (
7272

7373
var queued = await session.Connection.ExecuteScalarAsync<int>(
7474
"""
75-
WITH clear_attempts AS (
75+
WITH target_docs AS (
76+
SELECT id FROM documents
77+
WHERE tenant_id = @TenantId
78+
AND status IN ('finalized', 'completed', 'review_ready', 'failed')
79+
),
80+
clear_attempts AS (
7681
DELETE FROM extraction_attempts
7782
WHERE tenant_id = @TenantId
83+
AND document_id IN (SELECT id FROM target_docs)
7884
RETURNING 1
7985
),
8086
clear_fields AS (
8187
DELETE FROM extracted_fields
8288
WHERE tenant_id = @TenantId
89+
AND document_id IN (SELECT id FROM target_docs)
8390
RETURNING 1
8491
),
8592
clear_profiles AS (
8693
DELETE FROM case_profiles
8794
WHERE tenant_id = @TenantId
95+
AND document_id IN (SELECT id FROM target_docs)
8896
RETURNING 1
8997
),
9098
reset_docs AS (
9199
UPDATE documents
92100
SET status = 'uploaded', updated_at = now()
93-
WHERE tenant_id = @TenantId
94-
AND status IN ('finalized', 'completed', 'review_ready', 'failed')
101+
WHERE id IN (SELECT id FROM target_docs)
95102
RETURNING 1
96103
)
97104
SELECT COUNT(*)::int FROM reset_docs

src/Services/Northwoods.Api/Endpoints/ReviewEndpoints.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,21 @@ INSERT INTO audit_events (document_id, tenant_id, event_type, actor_id, details)
297297
new { DocId = id, TenantId = authContext.TenantId },
298298
session.Transaction)).ToList();
299299

300+
var discoveredFieldRows = (await session.Connection.QueryAsync<(string field_key, string extracted_value)>(
301+
"""
302+
SELECT field_key, COALESCE(corrected_value, extracted_value) AS extracted_value
303+
FROM extracted_fields
304+
WHERE document_id = @DocId AND tenant_id = @TenantId AND COALESCE(is_discovered, false) = true
305+
""",
306+
new { DocId = id, TenantId = authContext.TenantId },
307+
session.Transaction)).ToList();
308+
300309
var searchText = CaseProfileText.BuildFinalized(
301310
doc.template_id,
302311
request.Fields,
303312
ocrSegments.Select(o => (o.field_key, o.raw_value)),
304-
request.ReviewerNote);
313+
request.ReviewerNote,
314+
discoveredFieldRows.Select(d => (d.field_key, d.extracted_value)));
305315

306316
string? embeddingLiteral = null;
307317
var apiKey = openAiApiKey;

src/Workers/Extraction.Worker/ExtractionBackgroundService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ ON CONFLICT (document_id, field_key) DO UPDATE
275275
Confidence = result.SystemConfidence
276276
},
277277
tx);
278+
279+
if (canPersistAttempts)
280+
{
281+
await ExtractionPipelineService.PersistExtractionAttempts(conn, tx, docId, tenantId, extractionRunId, result, ct);
282+
}
278283
}
279284

280285
if (canPersistCaseProfiles)

src/Workers/Extraction.Worker/Services/ExtractionPipelineService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ internal static class ExtractionPipelineService
6565
discoveredResults.Add(FieldConsensus.Resolve(key, attempts));
6666
}
6767

68+
discoveredResults.Sort((a, b) => string.Compare(a.FieldKey, b.FieldKey, StringComparison.OrdinalIgnoreCase));
6869
return (schemaResults, discoveredResults);
6970
}
7071

src/Workers/Extraction.Worker/Worker.Testing.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed partial class Worker
2020
internal static FieldExtractionResult ResolveConsensusForTests(string key, IReadOnlyList<ExtractionCandidate> attempts)
2121
=> FieldConsensus.Resolve(key, attempts);
2222

23+
/// <summary>Returns schema fields only. Use RunExtractionPipelineWithDiscoveredForTests to get both.</summary>
2324
internal static async Task<IReadOnlyList<FieldExtractionResult>> RunExtractionPipelineForTests(
2425
ExtractionContext context,
2526
IReadOnlyList<string> fieldKeys,
@@ -30,6 +31,14 @@ internal static async Task<IReadOnlyList<FieldExtractionResult>> RunExtractionPi
3031
return schemaFields;
3132
}
3233

34+
/// <summary>Returns both schema fields and discovered fields.</summary>
35+
internal static Task<(IReadOnlyList<FieldExtractionResult> SchemaFields, IReadOnlyList<FieldExtractionResult> DiscoveredFields)> RunExtractionPipelineWithDiscoveredForTests(
36+
ExtractionContext context,
37+
IReadOnlyList<string> fieldKeys,
38+
IReadOnlyList<IExtractionProvider> providers,
39+
CancellationToken cancellationToken)
40+
=> ExtractionPipelineService.RunExtractionPipeline(context, fieldKeys, providers, cancellationToken);
41+
3342
/// <summary>
3443
/// Determines the document status based on ADR 005 confidence tiers.
3544
/// Returns (status, autoAccepted, requiresAttention).

0 commit comments

Comments
 (0)