checkai/docs/jev-criteria-routing.md (ENG-810). Shared helpers so
other paths can ask the same questions are ENG-829; the UEF bundle path landed
the first slice of that lift (see Next).
The split
Jev sits after extract and before the human gate. Its answer is advisory
context for the operator (or the agent), never a decision the pipeline acts on
by itself.
Source paths and where the referee sits
The Choice set
Thetarget Choice is the set of published UEF ingress targets and nothing
else. This was locked on 2026-09-18 (PRO-388, Ontology Architect comment),
with Leads added on 2026-09-22, and is enforced in code: IMPORT_TARGET_CRITERIA in importDecisionModel.ts is a
Record<ImportTargetChoice, string> derived from IMPORT_TARGETS, so a target
added to the registry without a criterion is a compile error, and a criterion
for a target that is not in the registry is one too.
Required paths are read from
uefCatalog.ts (required: true), not
maintained here; hardRequiredPaths marks the ones a waiver can never clear.
Two escapes are always offered, because a Choice with no way out forces
the nearest target onto data that belongs to none:
The out-of-criteria list in
OTHER is the Ontology lock’s own list (Dept /
Workflow / Phase / Series / Work Item). Those shapes are not targets, and Jev
is told so explicitly rather than left to pick MEMBER for a department
roster.
Nested shapes. The catalog publishes events[].members[],
events[].shifts[] and events[].pack_items[] today. schedule_items is
named in the lock as “nested when shipped” but does not exist in
uefCatalog.ts yet, so it is not a Choice option and no criterion references
it. A nested shape is
never a top-level Choice: an events sheet with crew columns is EVENT, and
the members are part of its completeness, not a second target.
The three questions
The wording that ships is inIMPORT_DECISION_QUESTIONS; this is what each
question means independent of source.
target — Choice over the set above. “Which target does this data
actually describe?” Judged from the shape of the data (columns, value types,
nesting), not from the target the operator or partner declared. The declared
target is in the state so the answer can agree or disagree with it;
matches_declared in the result records which.
mapping_confidence — Score, 4 levels. “How well does the proposed
source → catalog-field map fit the chosen target?” Levels describe situations,
low to high: most columns unmapped or mis-mapped · obvious columns mapped but
several meaningful ones wrong or missing · nearly everything plausible with one
or two doubts · every meaningful column on the right field. The float is a
weighted mean over levels, thresholded by host code; it is not P(correct).
ready_to_commit — Boolean. “Is this settled enough to move on to
preview and commit without another look at the mapping?” The answer is P(true).
It is judged against the required fields for the target, never against how
much of the full catalog was used — most catalog fields have no reason to
appear in any one source. Whether a required field is actually satisfied
(mapped, defaulted, or waived) is computed deterministically by the host and
stated in the state as fact; Jev is told that line is the authority.
The three are batched in one evaluate call and combined in host code. They
are not folded into one score, and Jev is never asked to combine them.
The recommendation rule
ready_to_commit.recommendation is proceed only when every signal agrees:
ready_to_commit.probabilityis at or above the confidence floor, and- the chosen target is the declared target and carries a probability at or above the same floor (no distribution counts as not confident; an escape never matches), and
mapping_confidencesits in the top half of its scale, and- the deterministic required-field check is empty.
ask_operator. Confidence only ever makes the outcome more
cautious; a high answer never skips the preview, the confirmation, or the
operator.
The state contract for a new source path
To ask the referee, a path builds onestate string. Whatever the source, it
must carry:
Caps: 5 rows, 6000 characters. Cell values never leave (
scrubValue), and
the call runs with zero data retention, no retries, and a hard timeout.
For a source that has no column map — a UEF bundle, a CheckAI extraction — the
same lines still apply with “field present in the document” in place of
“column mapped”: the declared target_type, which required paths are set,
which optional paths are set, and type-redacted samples of a few entries.
importUefDecisionModel.ts is that second state builder for UEF bundles; it
asks the same questions and uses the same recommendation rule. The question
wording names both cases (“columns or fields”) so one set serves both.
File ingestion
Files uploaded to a gig are read by the analyzer, merged per batch, reviewed, and committed. The analyzer and merge model keep their flat output schemas;ingestionPipeline.ts and mergeExecutor.ts convert that output to a UEF
2.0 proposal (schema_version 2.x) before it is stored. A proposal holds one
gig: events[0].days[].run_of_show[] cues with start / end clocks,
events[0].shifts[].crew[], events[0].ledger_items[], and leads[].
Selection is by document path. Review and commit name rows in selected:
$.events[0].days[1].run_of_show[2], $.events[0].shifts[3].crew[0],
$.events[0].ledger_items[4], $.leads[1]. A path that names no row is a
400. Batch reviews list cues dated outside the gig in out_of_range_paths.
The old schedule_idx / member_idx / ledger_idx / lead_idx request
fields are gone; a request that still sends one is a 400, and so is an
edited (or field-coverage uef) document that is not UEF 2.0.
Stored proposals must be UEF 2.0. Reviews, commits and field coverage
read only a 2.0 proposal. A file extraction or batch merge stored before 2.0
(one npm run ingest:convert-proposals did not convert) fails closed with a
409 PROPOSAL_PREDATES_UEF2. Retry the file to analyze it again (retry
accepts a ready file only when its proposal predates 2.0); for a batch, retry
its files and re-merge. A merge whose inputs include such a file fails with
the same message. The conversion script carries its own 1.0 reader; nothing else in
api-events reads 1.0.
Commit uses the shared import writer. ingestCommit.ts and
ingestDocument.ts, inside one transaction that holds
lockNonArchivedEvent (an archived gig is a 409):
- Keep the selected paths of the stored proposal.
- Stamp the gig from the target
Event: title and dates, used for parsing only. Ingestion never writes the gig header. - Drop crew with no email or phone, reported as skipped.
- Move cues on the dateless placeholder day onto the gig’s first day.
- Parse with
readUefDocument, then write cues, invitations, ledger lines, and leads with the shared pieces (applyImportedEventChildrenTx,commitLeadsTx).
errors with their
path, and error_rows counts them. These rows are skipped:
- a cue with a bad start time, a cue that ends at or before its start, and a cue dated outside the gig;
- a crew member with no email or phone;
- a lead with no email, and a ledger line with no type or description, sent in an edited document;
- a ledger line refused on its own fields (type, amount, status).
EventFieldDriftError), an archived or missing gig, old imported
ledger lines that cannot be reconciled with the new ones, a writer failure
for a whole day, a lead the shared lead writer refuses, and a document
problem that no single row owns. The stored committed copy and the
contributing-file list hold only the rows that were written. A commit with
skipped rows is committed; a commit where every selected row was skipped and
nothing else changed leaves the file READY_FOR_REVIEW and the batch merge
READY.
Preview runs the same plan through previewImportIntoEvent, so its counts
use the commit’s rules. Venue and location go through
applyEventFieldsInTx in the same transaction: the keys in
event_field_keys, or only empty fields when the caller sends none.
Rules that hold on every path
- Fail open. Flag off, no gateway key, timeout, gateway error, or an
answer outside the option set all resolve to
null, and the response is byte-identical to the flag being off. - Advisory only. The answer is context.
commit_import,import_file, MCP confirmation tokens, and the wizard’s preview → commit steps are untouched. A high score is not consent. - Asymmetric. Low confidence adds caution; high confidence removes none.
- No invented types. Choice options are Ontology-published ids only. New formats and sources add extract coverage, never new headers or targets.
- One referee per decision. A decision is judged by Jev on the Gateway or by classifier.dev, not both.
- Jev never generates. Extraction and drafting stay on the language model
or the heuristic mapper. Jev may pick a catalog field for a column from a
closed option list (
chooseFieldsWithJev), and otherwise only judges what the extractor produced. - Every ingestion path uses Jev. A new path that maps outside data into
our schema calls
chooseFieldsWithJev(key/column → field) or asks the referee questions here, throughshared/jev/jevEvaluate.ts. A typed provider API whose fields are mapped by fixed code is not such a path (see the table above). On by default when a gateway key is set; the per-path*_JEV_ENABLED=falseis a kill switch, not an opt-in.
Rollout
Since 2026-09-25 every Jev layer defaults on whereverAI_GATEWAY_API_KEY is
set; each path’s *_JEV_ENABLED=false turns it off. Answers only add a field
to the response and a jev.decision log line, so they fail open by
construction. Configuration, the header opt-in, and
the tuning history are in
Imports Jev decisions → Configuration.
Roll a new path out the same way: shadow first (log the answer, change
nothing), read the decisions against real files, then surface it.
Next
- Shared helpers (ENG-829), remainder. Every Jev call now goes through
shared/jev/jevEvaluate.ts. The question set, the state caps, and the recommendation rule (recommendImportDecision/judgeImportDecision) are shared by the spreadsheet and UEF paths; paste-fill and pricing-knowledge still carry their own question sets. Document-level referee forDone: behindPOST …/imports/uef.IMPORT_UEF_JEV_ENABLED, fail open, first 10 documents per request.- Folder-level question.
folderIngestdetection is deterministic; a Score over the whole plan (every file recognised, load order sound) is the natural next Choice/Score once per-file decisions are trusted. - Pin the model id (
IMPORT_JEV_MODEL_ID) to a dated version once thresholds are tuned, so a model update cannot move them silently.