mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(documents): bidirectional labs ↔ documents cross-link
Two directions, folding the vault and labs pipelines into one linking path without weakening either manual flow. Vault document → lab values (auto-stage). After the per-document auto-index job transcribes a stored document, a lab-looking document is run through the same inbound extraction the manual "Extract" button uses, but the facts land PENDING for the existing human review — nothing is committed. Gated on both the documents and labs modules plus the auto-AI-read consent gate the auto-index external path already uses; a text-structuring pass over the transcription the index just produced, so no second vision call is spent. The staging is a guarded STORED→EXTRACTED transition, and a keyword heuristic (or an explicit LAB_RESULT kind) keeps an arbitrary photo from burning a call. Everyone without the opt-in keeps the manual extract button. Lab scan → vault. A lab report scanned in the labs area is now also filed into the Documents vault (kind LAB_RESULT) through the existing upload — encrypted at rest, EXIF-stripped, thumbnailed, sha256-deduped — and the committed labs are cross-linked back to it with the same APPROVED-observation provenance the manual vault → labs path produces. Text-mode scans keep the image on-device and file nothing. Idempotent: a re-commit inserts no rows and links nothing new, and the vault dedupe means no duplicate document. Owner-scoped and module-gated on both sides; the cross-link is best-effort so it never fails the authoritative write.
This commit is contained in:
1 parent
502783a3ea
commit
73de8f6a52
10 files changed
+790
-13
No files matched your search
@@ -26,6 +26,10 @@ import { withIdempotency } from "@/lib/idempotency";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { resolveOrMintBiomarker } from "@/lib/labs/biomarker-store";
|
||||
import { serialiseLabResult } from "@/lib/labs/serialise";
|
||||
import {
|
||||
linkOcrLabsToVaultDocument,
|
||||
type InsertedLabForLink,
|
||||
} from "@/lib/labs/vault-link";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import {
|
||||
ocrCommitSchema,
|
||||
@@ -121,6 +125,9 @@ async function commitOcrRows(request: NextRequest) {
|
||||
|
||||
const inserted: ReturnType<typeof serialiseLabResult>[] = [];
|
||||
const skipped: OcrSkippedRowDto[] = [];
|
||||
// S9 — the rows actually written, kept for the vault cross-link so a re-commit
|
||||
// (every row a duplicate → nothing inserted) links nothing new.
|
||||
const linkable: InsertedLabForLink[] = [];
|
||||
// Tracks the keys already written in THIS request so an in-document duplicate
|
||||
// is caught even before the prior row is visible to a live query. The
|
||||
// mint-then-create on a mid-row failure can leave an orphan biomarker, which
|
||||
@@ -165,6 +172,28 @@ async function commitOcrRows(request: NextRequest) {
|
||||
|
||||
writtenInBatch.add(key);
|
||||
inserted.push(serialiseLabResult(created, biomarker));
|
||||
linkable.push({
|
||||
labResultId: created.id,
|
||||
analyte: created.analyte,
|
||||
value: created.value,
|
||||
valueText: created.valueText,
|
||||
unit: created.unit,
|
||||
referenceLow: created.referenceLow,
|
||||
referenceHigh: created.referenceHigh,
|
||||
takenAt: created.takenAt,
|
||||
});
|
||||
}
|
||||
|
||||
// S9 — cross-link the freshly inserted labs to the vault document the client
|
||||
// filed the scanned bytes into. Best-effort: the labs are the authoritative
|
||||
// write, so a link failure (module off, foreign / missing document) never
|
||||
// fails the commit. Owner + module checks live inside the linker.
|
||||
if (parsed.data.documentId && linkable.length > 0) {
|
||||
await linkOcrLabsToVaultDocument(
|
||||
user.id,
|
||||
parsed.data.documentId,
|
||||
linkable,
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
await auditLog("labs.ocr.commit", {
|
||||
|
||||
Reference in new issue
Block a user