feat(ocr): add durable review schema
This commit is contained in:
parent
1898eeb002
commit
936881b38f
2 changed files with 128 additions and 0 deletions
61
migrations/002_ocr_review.sql
Normal file
61
migrations/002_ocr_review.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS rag_ocr_jobs (
|
||||||
|
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
version_id uuid NOT NULL,
|
||||||
|
document_id text NOT NULL,
|
||||||
|
remote_job_id text NULL,
|
||||||
|
remote_idempotency_key text NOT NULL,
|
||||||
|
state text NOT NULL CHECK (state IN ('queued', 'running', 'succeeded', 'failed')),
|
||||||
|
requested_pages integer[] NOT NULL,
|
||||||
|
completed_pages integer NOT NULL DEFAULT 0 CHECK (completed_pages >= 0),
|
||||||
|
config_version text NOT NULL,
|
||||||
|
attempt_count integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
||||||
|
heartbeat_at timestamptz NULL,
|
||||||
|
lease_expires_at timestamptz NULL,
|
||||||
|
next_attempt_at timestamptz NULL,
|
||||||
|
error_code text NULL,
|
||||||
|
error_detail text NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
started_at timestamptz NULL,
|
||||||
|
completed_at timestamptz NULL,
|
||||||
|
UNIQUE (version_id, document_id),
|
||||||
|
FOREIGN KEY (version_id, document_id)
|
||||||
|
REFERENCES rag_version_documents(version_id, document_id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rag_document_pages (
|
||||||
|
version_id uuid NOT NULL,
|
||||||
|
document_id text NOT NULL,
|
||||||
|
page_number integer NOT NULL CHECK (page_number > 0),
|
||||||
|
extraction_method text NOT NULL CHECK (extraction_method IN ('native', 'ocr', 'blank')),
|
||||||
|
native_text_hash char(64) NULL,
|
||||||
|
ocr_text_hash char(64) NULL,
|
||||||
|
candidate_text_hash char(64) NULL,
|
||||||
|
reviewed_text_hash char(64) NULL,
|
||||||
|
metrics jsonb NOT NULL DEFAULT '{}',
|
||||||
|
risk_tokens jsonb NOT NULL DEFAULT '[]',
|
||||||
|
blocked_reason text NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (version_id, document_id, page_number),
|
||||||
|
FOREIGN KEY (version_id, document_id)
|
||||||
|
REFERENCES rag_version_documents(version_id, document_id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rag_review_corrections (
|
||||||
|
correction_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
version_id uuid NOT NULL,
|
||||||
|
document_id text NOT NULL,
|
||||||
|
page_number integer NOT NULL,
|
||||||
|
line_id text NOT NULL,
|
||||||
|
expected_line_hash char(64) NOT NULL,
|
||||||
|
replacement_text text NOT NULL,
|
||||||
|
reviewed_by text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
FOREIGN KEY (version_id, document_id, page_number)
|
||||||
|
REFERENCES rag_document_pages(version_id, document_id, page_number) ON DELETE RESTRICT,
|
||||||
|
UNIQUE (version_id, document_id, page_number, line_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS rag_one_pending_ocr_identity
|
||||||
|
ON rag_source_versions(source_id, original_manifest_hash, processing_fingerprint, metadata_hash)
|
||||||
|
WHERE source_content_hash IS NULL
|
||||||
|
AND state IN ('pending', 'indexing', 'review_required');
|
||||||
67
tests/catalog/migration-002.test.ts
Normal file
67
tests/catalog/migration-002.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
const migrationUrl = new URL("../../migrations/002_ocr_review.sql", import.meta.url);
|
||||||
|
|
||||||
|
async function readMigration(): Promise<string> {
|
||||||
|
return readFile(migrationUrl, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("migration 002 persists durable OCR jobs with idempotency and lease fields", async () => {
|
||||||
|
const sql = await readMigration();
|
||||||
|
|
||||||
|
assert.match(sql, /CREATE TABLE IF NOT EXISTS rag_ocr_jobs/i);
|
||||||
|
assert.match(sql, /remote_job_id text NULL/i);
|
||||||
|
assert.match(sql, /remote_idempotency_key text NOT NULL/i);
|
||||||
|
assert.match(sql, /requested_pages integer\[\] NOT NULL/i);
|
||||||
|
assert.match(sql, /state text NOT NULL CHECK \(state IN \('queued', 'running', 'succeeded', 'failed'\)\)/i);
|
||||||
|
assert.match(sql, /attempt_count integer NOT NULL DEFAULT 0/i);
|
||||||
|
assert.match(sql, /heartbeat_at timestamptz NULL/i);
|
||||||
|
assert.match(sql, /lease_expires_at timestamptz NULL/i);
|
||||||
|
assert.match(sql, /next_attempt_at timestamptz NULL/i);
|
||||||
|
assert.match(sql, /UNIQUE \(version_id, document_id\)/i);
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/FOREIGN KEY \(version_id, document_id\)\s+REFERENCES rag_version_documents\(version_id, document_id\) ON DELETE RESTRICT/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("migration 002 stores auditable page extraction hashes, metrics, and risks", async () => {
|
||||||
|
const sql = await readMigration();
|
||||||
|
|
||||||
|
assert.match(sql, /CREATE TABLE IF NOT EXISTS rag_document_pages/i);
|
||||||
|
assert.match(sql, /page_number integer NOT NULL CHECK \(page_number > 0\)/i);
|
||||||
|
assert.match(sql, /extraction_method text NOT NULL CHECK \(extraction_method IN \('native', 'ocr', 'blank'\)\)/i);
|
||||||
|
assert.match(sql, /native_text_hash char\(64\) NULL/i);
|
||||||
|
assert.match(sql, /ocr_text_hash char\(64\) NULL/i);
|
||||||
|
assert.match(sql, /candidate_text_hash char\(64\) NULL/i);
|
||||||
|
assert.match(sql, /reviewed_text_hash char\(64\) NULL/i);
|
||||||
|
assert.match(sql, /metrics jsonb NOT NULL DEFAULT '\{\}'/i);
|
||||||
|
assert.match(sql, /risk_tokens jsonb NOT NULL DEFAULT '\[\]'/i);
|
||||||
|
assert.match(sql, /blocked_reason text NULL/i);
|
||||||
|
assert.match(sql, /PRIMARY KEY \(version_id, document_id, page_number\)/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("migration 002 makes review correction targets unique and page-bound", async () => {
|
||||||
|
const sql = await readMigration();
|
||||||
|
|
||||||
|
assert.match(sql, /CREATE TABLE IF NOT EXISTS rag_review_corrections/i);
|
||||||
|
assert.match(sql, /expected_line_hash char\(64\) NOT NULL/i);
|
||||||
|
assert.match(sql, /replacement_text text NOT NULL/i);
|
||||||
|
assert.match(sql, /reviewed_by text NOT NULL/i);
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/FOREIGN KEY \(version_id, document_id, page_number\)\s+REFERENCES rag_document_pages\(version_id, document_id, page_number\) ON DELETE RESTRICT/i
|
||||||
|
);
|
||||||
|
assert.match(sql, /UNIQUE \(version_id, document_id, page_number, line_id\)/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("migration 002 prevents duplicate non-terminal OCR identities", async () => {
|
||||||
|
const sql = await readMigration();
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
sql,
|
||||||
|
/CREATE UNIQUE INDEX IF NOT EXISTS rag_one_pending_ocr_identity\s+ON rag_source_versions\(source_id, original_manifest_hash, processing_fingerprint, metadata_hash\)\s+WHERE source_content_hash IS NULL\s+AND state IN \('pending', 'indexing', 'review_required'\)/i
|
||||||
|
);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue