Deploying an artifact

One artifact, three execution surfaces — all producing identical integers.

1. Python runtime (any environment with Python ≥ 3.10)#

compileml.runtime imports only the standard library — a property enforced by a unit test that parses every runtime module's imports. That means:

  • Minimal serverless images. A function handler needs json, bisect, and the artifact — no numpy, no scikit-learn, no model server.
  • Vendoring works. Copying src/compileml/runtime/ into a constrained codebase gives you the full decision path with zero dependency review.
from compileml.runtime import load_artifact, decide

ARTIFACT = load_artifact("decision.json")        # verify=True by default

def handler(event, context):
    return decide(ARTIFACT, event["features"], explain=event.get("explain", True))

Operational note: explain everything — the default. A fully explained decision costs under a millisecond, and because attribution is aggregated per tree that cost does not grow with feature count. It is real-time against any credit-decisioning SLA, and it keeps one payload shape flowing through downstream systems with the explanation stored as part of the decision record. decide(…, explain=False) remains available as the faster score-only path for bulk pre-screening where no decision is communicated to a customer. Full-book batch re-explanation, once the expensive case, is now linear in trees; see the benchmark for measured figures.

Missing values follow the artifact's missing_policy — "baseline" re-applies the training-time imputation at decision time; "reject" refuses the row. NaN never routes silently through a tree.

2. SQL (warehouses, batch decisioning)#

compileml export decision.json --target sql --dialect ansi --out scorer.sql

One generated query: nested CASE-WHEN trees accumulating the artifact's integers, clamp, display conversion, band ladder, and the calibrated PD in ppm. Requirements: IEEE-754 double columns and 64-bit integer division truncating toward zero — satisfied by PostgreSQL, SQLite, DuckDB, BigQuery, and peers.

The test suite executes the generated SQL in a real engine and asserts integer equality with the Python runtime on every row — raw_micro, latent_int, band, pd_ppm.

Impute before the query (per the artifact's baseline); SQL NULL comparisons would silently skip branches, so the exporter's contract is non-null inputs.

Reason codes in the warehouse#

compileml export decision.json --target sql --explain --top-k 4 --out scorer.sql

--explain (export_sql(..., explain=True)) adds, on every row, reason_neg_<s>_code / reason_neg_<s>_impact and reason_pos_<s>_code / reason_pos_<s>_impact for slots 1 to top_k, NULL where a slot is empty. They are the codes and display-scale integer impacts decide(..., explain=True) returns, in order; the test suite executes the query in SQLite and checks them slot by slot, including an artifact built to force ties. One row comes out per row in, and no row key is needed: a feature's rank within its row is counted with CASE arithmetic rather than window functions. Working columns used along the way are prefixed cml_; select the ones you need.

The working steps are MATERIALIZED CTEs, because later steps refer to earlier columns many times and an engine that inlines them re-derives every reference — on a 23-feature, 30-tree artifact, SQLite took 492 s for 50 rows inlined and 0.18 s materialized. That sets the engine floor for --explain: SQLite 3.35+, PostgreSQL 12+, DuckDB. Engines without MATERIALIZED (BigQuery, Snowflake, SQL Server) can run the score-only query but not this one.

The query grows with the square of the feature count. Measured in SQLite on a 120-tree artifact:

featuresscore-only queryexplain query1,000 rows
230.03 MB0.56 MB0.40 s
500.03 MB0.93 MB0.66 s
1000.03 MB2.21 MB1.54 s

An artifact whose attribution is not exact is refused with EXPLAIN_NOT_EXACT, as in the COBOL export below.

3. COBOL (mainframes, core banking)#

compileml export decision.json --target cobol --program-id CMLSCORE --out scorer.cob

A self-contained >>SOURCE FORMAT FREE program: the artifact's leaf integers verbatim (ADD 21077 TO F-ACCUM-MICRO), the spec's integer division formula in COMPUTE, a strict-< EVALUATE band ladder, and the calibration table as a second EVALUATE that leaves the calibrated PD in F-PD-PPM. Compiles under GnuCOBOL and Enterprise COBOL 6+; CI compiles the export and checks latent, band and PD against the Python runtime row by row, for every calibration mode.

  • Feature inputs are COMP-2 (IEEE binary64). For decimal-arithmetic targets, compile the artifact with build_artifact(threshold_decimals=…) so every runtime — Python included — compares the identical quantized thresholds (spec §11).
  • Scope: score, band and calibrated PD by default.

Reason codes on the mainframe#

compileml export decision.json --target cobol --explain --top-k 4 --out scorer.cob

--explain (export_cobol(..., explain=True)) adds exact attribution: after scoring, the program leaves the top adverse and favorable reasons in REASON-NEG-CODE(i) / REASON-NEG-IMPACT(i) and REASON-POS-CODE(i) / REASON-POS-IMPACT(i), with REASON-NEG-COUNT and REASON-POS-COUNT saying how many slots are filled. The codes and display-scale integer impacts are the ones decide(..., explain=True) returns, in the same order; CI compiles the program under GnuCOBOL and checks them row by row, including forced ties.

It emits codes and impacts, not message text. The customer-facing wording belongs to the institution's letter templates, keyed by code; carrying long localized strings through PIC X fields would add weight without adding anything the templates don't already do.

Each tree's features and the artifact's baseline are known when the program is generated, so every comparison against a baseline value is resolved at export: a tree's subset walks become short, fixed IF trees over the real inputs. The program grows with tree count, which is why this is opt-in.

When the exporter refuses, it raises ExportError with a stable code, and the CLI prints it and exits with status 2:

CodeCauseWhat to do
EXPLAIN_NOT_EXACTThe artifact's attribution is not exact (whitebox depth > 2), so reasons would not reconcile to the score.Compile at depth ≤ 2, or export without --explain.
REASON_CODE_NOT_ASCIICOBOL only. A reason code — from the dictionary, or the NEGATIVE_<name> fallback — is not printable ASCII, which mainframe character sets would not carry unchanged.Give that feature an ASCII code in the reason dictionary, or suppress it.

Which surface for what#

SurfaceReturnsTypical role
runtime decide(explain=True)band, PD, exact reasonsdecisioning API, adverse-action notices
runtime decide(explain=False)band, PD, latentbulk pre-screening (no customer-facing decision)
SQL exportband, PD, latent per row; reason codes and impacts with --explainwarehouse batch, portfolio re-score
COBOL exportband, PD, latent; reason codes and impacts with --explaincore-banking / mainframe rails

Whatever the surface, the integers agree — that's the point.