@amirandap/data-review-core
v0.1.0
Published
Framework-agnostic core for LLM extraction + human-in-the-loop review: the shared field/prompt-version vocabulary, the JSON-Schema round trip, OCR token matching, a fetch client with actionable errors, the ParseResult contract, and the date/timezone helpe
Downloads
240
Maintainers
Readme
@amirandap/data-review-core
Canonical source as of 2026-08-05 — was
@amirandap/data-review-ui/packages/core, consumed here as a submodule; now the reverse:@amirandap/data-review-uiand the sibling backend consume THIS copy as a submodule (see docs/CONSUMING.md in@amirandap/data-review-ui, which documents the mechanism this direction still uses). Change it here; port anything genuinely generic upstream is no longer the model — this repo IS upstream now.
The framework-agnostic contract shared by every extraction/review app. Pure TypeScript, zero runtime dependencies — the same module imports into a MUI app, a Tailwind/shadcn app, and a plain Node backend.
It exists because the same four things were written independently in three repos, and drifted:
| | Was in | Now |
|---|---|---|
| Field list → LLM schema | @amirandap/data-review-ui | sectionsToJsonSchema |
| LLM schema → field list | the WhatsApp consumer (lib/schemaFields.ts) | jsonSchemaToSections, parseSchemaFields |
| OCR word → form field matching | the sibling backend (OcrImagePreview.tsx) | matchTokensToValues |
| Engine result shape + cost | the sibling backend (services/base.py) | ExtractionResult, estimateCostUsd |
Install
Not published yet — see docs/CONSUMING.md in the
repo root. Apps that render with MUI can install @amirandap/data-review-ui
instead, which bundles this package and re-exports all of it.
The schema round trip
One field list drives both the review form and the extraction call:
import { sectionsToJsonSchema, jsonSchemaToSections } from '@amirandap/data-review-core';
// Forward — send this as OpenAI's response_format.
const { name, strict, schema } = sectionsToJsonSchema(sections);
// Back — render a form for a pipeline whose schema you did NOT generate.
const sections = jsonSchemaToSections(promptVersion.schema_json);The reverse direction handles what hand-written schemas actually contain:
$ref into $defs, anyOf: [X, {type:'null'}] for nullables, arrays of
objects. Top-level scalars become one section; each nested object or array
becomes its own. Unusable input returns [], never throws — it runs live
while a user types into a schema textarea.
OCR token matching
import { matchTokensToValues } from '@amirandap/data-review-core';
const overlayTokens = matchTokensToValues(detectedWords, formValues, { fields });
// matched → { matchedFieldKey, label } (render green)
// unmatched → bare (render yellow, click to fill)Matching ignores punctuation, case and separators (RD$ 6,180.25 matches a
6180.25 box; 809-555-1234 matches 8095551234), and an ISO date in the
form also matches its MM/DD/YYYY and MM/DD printed forms. Values with no
signal — empty, null, a bare 0 — are skipped, because a lone zero matches
half a utility bill and turns the overlay into noise.
The result contract
Every engine — an LLM call, a Tesseract subprocess, a local VLM — returns the same thing:
import { parseExtractionResult, qualityScore, isViable } from '@amirandap/data-review-core';
const result = parseExtractionResult(stdoutFromAnyEngine);
// { documentType, isTarget, confidence, data, qcErrors, qcWarnings,
// modelVersion, processingMs, costUsd, costMeta, extra }
qualityScore(result, ['nic', 'kwh', 'total_facturado']); // 0..n
isViable(result, { identityFields: ['nic'], valueFields: ['kwh'] });The wire format is snake_case JSON, identical to what the sibling backend's
Python engines already print, so no adapter is needed on either side. Unknown keys
survive a parse → extractionResultToWire round trip in extra, so an
engine-specific field is never silently dropped by passing through a generic
layer.
prompt_versions row adapters
import { promptVersionFromRow, promptVersionToRow } from '@amirandap/data-review-core';Between a prompt_versions row (see @amirandap/llm-file-pipeline's
migrations/0001_prompt_versions.sql; the sibling backend's MySQL table uses
the same column names) and the PromptVersion model the review UI renders.
promptVersionToRow deliberately emits no version, is_active or
created_at: the store assigns the version number, and activation is a
separate explicit step so a saved draft never silently becomes the live prompt.
