Skip to main content
Soundcheck does not build a native connector per source system. Every inbound path — a spreadsheet, a Planning Center or Master Tour export, a season folder, a partner’s UEF bundle, a CheckAI extraction from a contract — is turned into Universal Event Format by an extract step, and then Jev (TypeSafe AI’s evaluation-only model) referees the result with the same three typed questions. Jev cannot generate text: it picks from options we give it and returns probabilities. It can never invent a UEF type, a field path, or a cell value. This page is the source-agnostic contract those questions share. It is the engineering companion to PRO-388 (product home). The first land, live on dev behind a flag, is the spreadsheet path — see Imports Jev decisions (ENG-811) for that path’s exact state format, rules, and configuration. The CheckAI side is 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

The target 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 in IMPORT_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:
  1. ready_to_commit.probability is at or above the confidence floor, and
  2. 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
  3. mapping_confidence sits in the top half of its scale, and
  4. the deterministic required-field check is empty.
Anything else is 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 one state 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):
  1. Keep the selected paths of the stored proposal.
  2. Stamp the gig from the target Event: title and dates, used for parsing only. Ingestion never writes the gig header.
  3. Drop crew with no email or phone, reported as skipped.
  4. Move cues on the dateless placeholder day onto the gig’s first day.
  5. Parse with readUefDocument, then write cues, invitations, ledger lines, and leads with the shared pieces (applyImportedEventChildrenTx, commitLeadsTx).
A selected row that is invalid on its own data is skipped, and every other selected row still commits. Skipped rows come back in 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).
Anything else rolls the whole commit back: a database error, event-field drift (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, through shared/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=false is a kill switch, not an opt-in.

Rollout

Since 2026-09-25 every Jev layer defaults on wherever AI_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 for POST …/imports/uef. Done: behind IMPORT_UEF_JEV_ENABLED, fail open, first 10 documents per request.
  • Folder-level question. folderIngest detection 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.