Changelog¶
All notable changes are documented here. The project follows Semantic Versioning;
milestone releases are tagged on main.
[Unreleased]¶
[0.15.1] — Docs refresh for 0.10 to 0.15 — 2026-10-20¶
This release updates the documentation for v0.10.0 to v0.15.0. It has no code changes.
Changed¶
- New page "What's new in 0.10 to 0.15", first in the Home group of the nav, with screenshots from the release checks. The home page, why, use cases, getting started and roadmap pages describe run tracking, the MMM pack, causal models, run comparison, image data and deep learning. The roadmap shows v1.0 (MLflow export, system metrics, retention) as planned.
- New concept pages: run tracking, image collections and causal effects. Capabilities, representations, bundles, workflows, quotas, security and architecture cover v0.10 to v0.15.
- Guides, model pages and tutorials agree with the code: tracking, run comparison, image data, deep learning, the explorers, the CLI, the Python client, the HTTP API, MMM, causal, time series, sklearn and Guided mode.
- Reference and operations pages cover the base-dl image, the GPU class on Docker and Cloud
Run (not tested on real hardware), the new settings and the new warning codes. Every package
and plugin README is current, including
mapi-jaxandmapi-plugin-causal. - The Guided templates screenshot shows all eight templates.
[0.15.0] — Deep learning and image collections — 2026-10-20¶
Deep learning and image collections¶
You can now train neural networks on the platform. The new package mapi-jax has three
models: a network for tables, an image classifier and a classifier that starts from
pretrained weights. The training shows the training and validation curves while it runs,
logs images of the predictions, stops early, saves checkpoints and gives draws. Large image
sets upload as image collections: the client uploads only the new images, and an upload
that stops can continue. A new resource class gives a run one GPU.
Added¶
- Image collections: the dataset layout
collection. A version is a manifest (manifest.parquet), a summary (collection.json,CollectionInfo) and packs of images, stored one time for each tenant att/{tenant}/blobs/packs/{digest}.zip. A version can hold 1 000 000 images of different sizes. The pack address ispack_digest(size, crc32c, md5)over the digests that the store calculated. Each image is identified by its SHA-256, and each read makes sure that the SHA-256 is correct. Contracts inmapi_spec.collection. DatasetVersionCreate.collection(CollectionSource: a manifest upload, up to 64 part uploads, a base version andCollectionOptions). The ingest run reads the parts in ranges, makes sure that each image has the SHA-256 of its name, reads the image headers and writes the explorer views of a sample. The finalizer copies the parts to the shared pack path.- Resumable uploads and deduplication:
GET /v1/datasets/{dataset_id}/uploads/{upload_id}(UploadSession.received),POST .../uploads/{upload_id}/renewandPOST /v1/datasets/{dataset_id}/blobs/missing(BlobQuery,BlobMissing: the images that the base version does not have). - Client and CLI:
datasets.upload_collection,datasets.missing_images,datasets.get_upload,datasets.renew_upload;mapi datasets upload-images,mapi datasets missing-imagesandmapi datasets upload-weights. The parts are deterministic zip files, and a state file continues an upload. - Web app: the upload step Folder of images (labels from folders or from a manifest column, progress, retry and continue), and the collection summary on the version page.
mapi_sdk.collection:Collection, the lazy and seededCollectionLoader,ArrayImageLoader,ImageLoader,Batch,Augmentandto_xarray(max_samples, size).mapi_sdk.imagesdecodes images.mapi_sdk.testing.imagesmakes small test collections.- Model hooks
fit_batchesandscore_batches(Capabilities.input = "collection"). The harness gives these models a loader of the rows of the split. Other models get a seeded sample of a collection, as set bydata.collection(CollectionRead). - Model side tables:
ModelManifest.side_table_params,FitContext.side_tablesandResolvedModel.side_tables. A score request never needs a table that only the model reads. - The upload format
safetensors(WeightsOptions): a file of network weights becomes a weights dataset (weights(param)and the attributemapi_weights).mapi_sdk.safetensorsreads and writes safetensors files with numpy only. mapi-jax(packages/sdk-jax, Apache-2.0) on JAX, Flax (nnx) and Optax: the modelsdl.mlp,dl.cnn_classifieranddl.finetune_classifier, and the stepdl.embed(image features from a frozen pretrained network). The shared trainer logslossandaccuracy(orrmse) with the labelsplit, logs prediction images and the confusion matrix as media, stops early, can be cancelled, is deterministic for a seed, and saves checkpoints every N epochs. The models give draws from a last-layer Laplace approximation or from MC dropout.fit.checkpoint.every_epochs, and file checkpoints (ckpt-<key>.weights.safetensors,ckpt-<key>.opt.safetensors,ckpt-<key>.meta.json) with resume.tabular.image_logistic(model version 1.1.0) takesinput: features, for example the output ofdl.embed.- The resource class
GPU(one NVIDIA L4, 8 vCPU, 32 GiB) and the runner imagebase-dl(mapi-jax[cuda12]). The Cloud Run inventory has the Jobsbase-dl × {S, M, L, GPU}(Terraformgpu_jobs, default off). The docker executor asks for a GPU whenMAPI_DOCKER_GPUS=true. Otherwise, and with the local-process executor, a GPU run uses the CPU and shows the warningGPU_UNAVAILABLE. The price table has a GPU row with an accelerator price. - Plots:
ml.training_curves,ml.confusion_epochsandml.misclassified, with About cards. The fit adds them when the bundle has the training history. - About cards for
dl.mlp,dl.cnn_classifier,dl.finetune_classifieranddl.embed. - Documentation: the guide Deep learning, and the section Large image sets: collections of the guide Image data.
Changed¶
Checkpoint.every_drawsis optional. A checkpoint has exactly one ofevery_drawsandevery_epochs. Documents withevery_drawsdo not change.Capabilities.inputacceptscollection.DataLayoutandDatasetVersion.layoutacceptcollection.IngestJobtakes exactly one ofmapping,preset,assemblyandcollection.IngestFormathassafetensors.ResourceClasshasGPUas its last member.next_up()walks the CPU classes only, and quotas with a maximum class belowGPUrefuse it.ResourceClassSpechasacceleratorandaccelerators.ClassPriceandResourceUsagehave accelerator fields (left out when zero).validate_spectakesdataset_layoutandside_table_layouts: a collection takes no pipeline steps for a model that trains on batches, takes only row splits, and cannot be a side table. Validation refuses a spec that selectsGPUfor a model that does not use a GPU (the GPU Job runs only these models). It warns when a spec setsevery_epochsfor a model that cannot save checkpoints.- An upload whose file arrived has the status
uploaded. It stays usable after its PUT URL expires, so a collection upload that takes more than one hour, or that continues the next day, can make its version. The storage quota counts apendingupload until the housekeeper retires it. The housekeeper deletes an arrived upload that no version used after 7 days.get_uploadneedsupdateondataset, because its answer holds the signed PUT URL. When you renew an expired upload, the server does a check of the storage quota again. - The size of a collection version is the size of the packs that it uses, also the images in a part that the manifest does not use.
- The worker environment allows
JAX_PLATFORMS,XLA_PYTHON_CLIENT_PREALLOCATE,XLA_PYTHON_CLIENT_MEM_FRACTIONandCUDA_VISIBLE_DEVICES. - The images
mapi/base-bayesandmapi/base-mlhavemapi-jaxfor the CPU, so thatdl.embedworks in front of their models. The import-linter contracts forbidmapi_jax,jax,flaxandoptaxin the control plane and in the supervisor. - Upgrade the CLI and
mapi-clientto v0.15 to upload collections and weights. Older clients ignore the new fields.
Known gaps¶
- Cloud Run GPU jobs and the
base-dlCUDA image are not tested against GCP or on a GPU. - Deduplication is only against a base version of the same dataset.
- The platform keeps packs until the tenant is deleted.
- There are no thumbnails for each image beyond the gallery sample of the ingest.
- Collections take no pipeline steps (with a model that trains on batches) and no time splits.
- Workflows and side tables do not take collections.
dl.embedon a collection needsdata.collection(a sample). Usedl.finetune_classifierwithfreeze: backbonefor all images.- One GPU for each run. There is no training on more than one GPU or more than one machine.
- Pretrained weights must use the
mapi-jaxResNet layout. There is no conversion from PyTorch checkpoints.
[0.14.0] — Run comparison, media and image data — 2026-10-20¶
Run comparison, media, image data and classification¶
You can now compare runs and group them. The runs table shows metrics, params and tags as columns, and you can filter, sort and save the search as a view. The compare page shows 2 to 10 runs side by side. An experiment is a named group of runs. A run can also log images, figures and small tables. You can upload a zip of small images as a dataset, look at the images in a gallery, and classify them. The classification pack adds a multiclass classifier, LightGBM and CatBoost, new GLM families and multiclass plots.
Added¶
- Experiments: a named group of runs in a project (
GET/POST /v1/experiments,GET/PATCH/DELETE /v1/experiments/{experiment_id},POST /v1/experiments/{experiment_id}/runs). A run is in a maximum of one experiment (RunCreate.experiment_id,Run.experiment_id,RunPatch.experiment_id,WorkflowRunCreate.experiment_id). A workflow node run gets the experiment of its workflow run. A retry gets the experiment of the run that it retries. An archived experiment accepts no new runs and no retries.list_runshas the filterexperiment_id. - Saved views of the runs table (
/v1/run-views): a name and aRunViewQuery(the filters, the sort order, the columns, the tags and the other filters oflist_runs), shared in the project. A maximum of 200 views in a project. GET /v1/run-compare: the summaries, the metric histories, the params (withdiffers) and the media of 2 to 10 runs.GET /v1/run-keys: the metric series, param keys and tag keys of the runs of a project or an experiment, with the number of runs that have each.- Run media:
ctx.log_image,ctx.log_figureandctx.log_tableon the fit, score and node contexts. The supervisor uploads the files through a create-only POST policy on themedia/prefix of the run (RunSpec.media_prefix, outputmedia) and sends amediaevent. The server downloads each file, does a check of its SHA-256, size and format, and keeps a copy in the bundles bucket. A run can log 1000 items and 256 MiB.GET /v1/runs/{run_id}/media,/media/{media_id},/media-keys, andPOST /v1/runs/{run_id}/media(from a notebook, a maximum of 4 MiB). Thetrackingframe has the fieldmedia. mapi_spec.media: the media kinds and content types,sniff_image(PNG and JPEG headers, no image library),MediaTable,media_name,check_mediaand the limits.mapi_sdk.media:MediaItem,encode_png(numpy and zlib only) and the optionalMediaTrackerprotocol.- Web app: the runs table has a filter bar, a column picker for tracked columns, sort by a column, saved views, and check boxes to compare runs or to add them to an experiment. New pages: Experiments, the page of an experiment, and the compare page (curves, metric bars, param differences, plots side by side, and images with a step slider). The run page has a Media tab. The Notes tab moves the run to an experiment.
- Client and CLI:
runs.media,runs.media_keys,runs.get_media,runs.download_media,runs.log_image,runs.log_table,runs.keys,runs.compare,client.experimentsandclient.run_views.mapi experiments ...,mapi runs media,mapi runs log-image,mapi runs compare,mapi runs views ...,mapi runs ls --experiment/--view,mapi fit --experimentandmapi workflows run --experiment. - Image data, stage 1: the upload format
image_folder(a zip of PNG, JPEG or TIFF images and a manifest table,ImageFolderOptions). The ingest writespixels(image, y, x, channel)with the new dtypeuint8, the targetlabel(image)and the manifest columns as coordinates. It can resize the images (stretch,croporpad). The ingest refuses unsafe paths, images of more than 4096 × 4096 pixels, images with more than 8 bits, and datasets that are too large, with a message that gives the number of images that fit. - Data Explorer: the Gallery section (thumbnails of the images, an even sample for each label and split, and the counts of each facet), and colour (RGB) images in Images and fields. A label variable on the frames gives the classes of the mean images.
- The split kinds
column(hold out the rows with a value of a column, for examplesplit = test) andstratified(hold out the same share of each class, at random), with About cards. - Classification:
sklearn.multiclass_classifier(the probability of each class iny_hat(..., class)), the estimatorssklearn.HistGradientBoostingClassifier,lightgbm.LGBMClassifierandcatboost.CatBoostClassifier(the extramapi-sklearn[gbm]), and the metricaucfor multiclass predictions (the mean one-vs-rest AUC). tabular.hierarchical_glm(model version 1.1.0) has the familiesnegative_binomial,zero_inflated_poisson,zero_inflated_negative_binomial,beta,gammaandordinal.tabular.image_logistic: a Bayesian multinomial logistic regression on the main patterns (PCA) of small images. The fit logs the patterns and the class means as images.- Help me choose has the goal "Put items or images into classes" (
classify) and the data shape "Images (pictures of the same size)" (images). - Documentation: the guides Compare runs and use experiments and Image data.
Changed¶
core.roc_prandcore.confusionaccept a binary or a multiclass target. With a class dim on the prediction, or more than two observed classes,core.roc_prdraws one-vs-rest curves for each class with the macro AUC (classesselects the classes), andcore.confusionshows a K × K matrix with the accuracy and the macro F1. The binary behaviour does not change.sklearn.tabular_classifieraccepts the new classifier estimators.verify_output_objectsaccepts objects belowmedia/of the run prefix by default.mapi-runnerdepends on Pillow. The runner imagemapi/base-mlhas LightGBM, CatBoost andlibgomp1. The other runner images have Pillow, and do not have LightGBM or CatBoost. Themapi/serverimage has none of them.- New audit actions:
experiment.create,experiment.update,experiment.delete,run_view.saveandrun_view.delete. A media write through the API is arun.logevent. - Upgrade the CLI and
mapi-clientto v0.14 to use experiments, saved views, comparison and media. Older clients ignore the new fields.
Removed¶
mapi_spec.expand_sweepand the modulemapi_spec.sweep. Nothing called them.- New
Sweepdocuments: the server refuses them (422 SCHEMA_INVALIDat/kind). StoredSweepdocuments still load. Use a workflow with asearch.gridnode.
Known gaps¶
- A fit without a run (for example
fit_specin a notebook) keeps no logged media.tracking_mediaisFalse, andlog_image,log_figureandlog_tableonly check their arguments. To keep the items in memory, give the fit a recorder:FitHooks(tracker=MetricRecorder()). The items are then inMetricRecorder.media. - Workflow schedules and triggers start their runs in no experiment. To group such a run,
move it with
PATCH /v1/runs/{run_id}before it starts its nodes. Node runs that started before the move stay where they are. - The platform keeps media until you delete the tenant. There is no retention setting for media.
image_folderhas no inspect step. Errors about the columns of the manifest come from the ingest run.- Images must have 8 bits for each channel and one size for each dataset. A dataset has a maximum of 268 435 456 pixel values. The collection layout of v0.15 removes these limits.
- Workflow backtests do not make stratified split plans.
- There is no calibration plot for multiclass predictions.
- LightGBM and CatBoost are only in the
mapi/base-mlrunner image. - Validation refuses
log_scoreforsklearn.multiclass_classifier, as for the binary classifier, because the plugin gives point predictions. Usebrier.
[0.13.0] — Causal models and Bayesian state space — 2026-10-20¶
Causal inference and Bayesian state space¶
The platform can now measure the effect of a campaign, a launch or a test. Three new causal
models compare the actual values with a prediction of the values without the change: on one
series (causal.impact), in test markets against a mix of the other markets
(causal.synthetic_control) and in units with and without a change (causal.did). The result
of a geo-lift test becomes a lift test for mmm.bayesian. A new Bayesian structural time
series (ts.bsts) forecasts one series from its level, trend, seasonal patterns and drivers.
Each fit logs its results with the v0.11 tracking API.
Added¶
ts.bsts(mapi-plugin-ts): a Bayesian structural time series on the state-space models of pymc-extras. It has a level, a trend (level,local_linearorsmooth), a maximum of three seasonal patterns (one effect for each season, or harmonics) and drivers with one effect for all periods. The fit uses the exact Kalman likelihood. Missing periods are permitted. The bundle keeps aterminal_stategroup.forecaststarts from it, and thefiltermode adds new periods without a new fit. Thecomponentsgroup shows the level, the seasonal parts and the drivers withcore.decomposition.- A new plugin package,
mapi-plugin-causal(Apache-2.0), with three models. Each model writes the effect to the bundle groupimpact(actual values, values without the change, the effect in each period and the cumulative effect, in the units of the target):causal.impact: CausalImpact (Brodersen et al. 2015) on thets.bstscore. The model fits the periods before the change, with optional control series. Without control series, it is an interrupted time series.causal.synthetic_control: a Bayesian synthetic control for geo-lift tests. The weights of the other markets are positive with a sum of 1 (simplex), or have a horseshoe prior. The bundle groupdonor_weightskeeps the weights.causal.did: difference-in-differences and event study (Bayesian two-way fixed effects). The bundle groupevent_studykeeps one coefficient for each period before and after the change. Thecounterfactualmode sets the treatment to 0.
- New plot recipes, each with an About card and example data:
causal.impact(the actual values against the values without the change, the effect in each period and the cumulative effect),causal.pre_fit(the fit before the change),causal.donor_weightsandcausal.event_study. - The workflow node
model.lift_testmakes a lift test formmm.bayesianfrom theimpactgroup of a causal model version.model.fithas a new parametercalibration. - The workflow template
geo_lift_mmm: a geo-lift test, its lift test,mmm.bayesiancalibrated with the lift test, the plots and a report. - Guided templates
campaign_impact(Effect of a campaign,causal.impact) andgeo_lift(Geo-lift test,causal.synthetic_control), with About cards. The report ofgeo_lifthas a card that makes the lift-test entry for your marketing mix. - Help me choose has a new goal: "Measure the effect of a campaign, a launch or a test"
(
measure_impact). The four new models have About cards. - Run tracking:
ts.bstslogs its structure (bsts.*params), the noise and the error of the one-step-ahead predictions. The causal models log the total effect with its interval, the relative effect, the probability that the effect is more than zero and the error before the change. Synthetic control also logs the donor weights. DiD also logs the average effect and the event-study coefficients. - Conformance fixtures
e13_bsts(the plan fixture E13 is no longer deferred),causal_impact,geo_liftanddid_event_study. - Documentation: the pages Causal models and Bayesian structural time series and Measure the effect of a change.
Changed¶
- pymc-extras is now a declared dependency: of
mapi-plugin-ts, and of the new extramapi-pymc[pathfinder]for thepathfinderengine. The error message for a missing engine names the extra. Pathfinder does not write a progress table to the output. - The runner image
mapi/base-forecasthas pymc-extras andmapi-plugin-causal. Thecausal.*models run in it.mapi/base-bayeshas pymc-extras for Pathfinder. GuidedTemplate.familyhas the new valuecausal. Upgrade the CLI andmapi-clientto v0.13. A v0.12 client cannot read the list of Guided templates from a v0.13 server (mapi guided templatesfails for all templates). From v0.13, a client keeps a family that it does not know as text.- The
causal.*plot recipes are platform recipes: you can embed them, and the Guided share step offers them. Only the platform distributions can register ids in thecausalandchoicenamespaces. - A score output can be a difference (the scored array attr
difference_ofnames the reference output). Withoutput.units: original, the harness changes the level and the reference to the output units, then subtracts them. Theeffectofcausal.impactuses this, thus it isy - y_hatin the units of the data also when the pipeline transforms the target. - The Guided steps of
campaign_impactandgeo_liftdo not include the scenarios step (the templates have no tasks). The lift-test card of thegeo_liftreport uses the test markets of the revision that the last fit used. causal.didscores a payload with only some periods: the periods that the payload does not have take the adoption of the fit.causal.impactrefuses periods that are not evenly spaced (SCHEMA_INVALID), asts.bstsdoes. An impossible date inintervention_startorintervention_end(for example2024-02-30) isSETTINGS_INVALID. Two units or donors whose names give the same tracking label get different labels (_2,_3).
Known gaps¶
ts.bststakes one series. Each driver has one effect for all periods. A forecast does not use actual values that you know (use thefiltermode). A seasonal pattern with one effect for each season has a maximum period of 24 (use harmonics for longer periods).causal.impactfits the periods before the change only.causal.synthetic_controlhas no placebo tests and does not design a test (the choice of the markets, the power).- A lift test from two or more test markets is national.
- The time effects of
causal.didare fixed, thus the model does not forecast. When the units got the change at different times, the estimates can be wrong. - The lift-test card of the Guided report does not write into the MMM spec. Copy the entry.
[0.12.0] — MMM pack — 2026-10-20¶
MMM pack¶
The marketing-mix models get two new models, two new kinds of data and the charts that a
marketing team asks for. mmm.nested measures effects that go through an intermediate measure,
for example visits or awareness. mmm.multivariate measures effects on two or more results at
the same time. mmm.bayesian can now have channel effects that change over time, and counts
or shares as the result. Each MMM fit logs its return on spend and other summaries with the
v0.11 tracking API.
Added¶
mmm.nested(mmm-frameworkNestedMMM): a national marketing mix with one or more intermediate measures (mediatordim). Each measure can be measured in some periods only, or not at all (latent: true). Each channel has a direct effect and an indirect effect through each measure. The new derived quantitymediationgives the direct, indirect and total effect of each channel and the part that goes through the measures. Lift tests calibrate the total effect of a channel. The outputmediator_hatgives the fitted measures.mmm.multivariate(mmm-frameworkMultivariateMMM): a national marketing mix of two or more results (outcomedim) with correlated noise. Cross-effects between the results can behalo,cannibalizationorunconstrained. Scores, response curves and budget plans use the reduced form, thus a channel that moves one result also moves the results that it acts on. The new derived quantitycross_effectsgives each cross-effect.outcomes[].weightgives the value of one unit of each result in the budget plan. The KPI of the plan is then "weighted total of the outcomes", and a note of the plan gives the weights.mmm.bayesian1.1.0: time-varying channel effects. Put the channels intime_varying.channels. Their effect is a random walk over the fit window.time_varying.forecastsets the effect after the fit window:random_walk(default, the range becomes wider with the horizon),hold_lastormean. A forecast tells you which policy it used.mmm.bayesian1.1.0: the likelihoodspoissonandnegative_binomial(counts, log link) andbeta(shares between 0 and 1, logit link).media_effect_sigmasets the prior of the channel effects of these likelihoods. The contributions of the channels still add up toy_hat. The trend, the seasonality and the controls are then part of the baseline.- New derived quantities for every MMM:
adstock(the part of the effect of a unit of spend that comes in each later period, and the mean lag).mmm.bayesianandmmm.frequentistalso haveeffectiveness(the effect for each unit of spend in each period). - New plot recipes, each with an About card and example data:
mmm.roi: the return on spend of each channel with a range, and the marginal return (mROAS), against a break-even line.mmm.adstock: how the effect of spend decays over the next periods, with a range.mmm.waterfall: the contribution of each component to the total result.mmm.spend_effect: the share of spend against the share of the effect of each channel.mmm.lift_tests: the result of each lift test, with its error bar, next to the estimate of the model.mmm.effectiveness: the effect of a unit of spend over time, for time-varying channels.mmm.mediation: the direct and indirect effect of each channel (mmm.nested).mmm.cross_effects: the effects between the results (mmm.multivariate).core.energy: the energy plot of the sampler and the BFMI of each chain.core.prior_posterior: the prior and the posterior of each parameter, with their overlap.
- A fit with lift tests keeps the bundle group
lift_tests(each test and the estimate of the model for each draw). - Run tracking for every MMM: each fit logs the params
mmm.framework_version,mmm.n_channels,mmm.n_periods,mmm.n_cellsandmmm.link, and the metricsroi,roi_p05,roi_p95,mroiandcontribution_sharefor each channel (labelchannel) andbaseline_share.mmm.nestedandmmm.multivariatealso send the progress of each chain and the step metrics, asmmm.bayesiandoes. - Guided templates
mediated_mmm(Marketing mix with an intermediate measure,mmm.nested) andmulti_kpi_mmm(Marketing mix for related products,mmm.multivariate), with About cards. Help me choose recommends the two models and templates for "a few related numbers over time".
Changed¶
geo_mmm2.1.0: the template also keepsadstockand drawsmmm.roi.- The About cards of
mmm.nestedandmmm.multivariatenow tell what the models can do: national data, no control variables, and cross-effects that are associations. The card ofmmm.bayesiantells about time-varying effects, counts and shares. - Guided drafts: a question that allows missing values also suggests columns that are mostly
empty (for example a monthly survey). The labels of a wide covariate that has its own dim
keep their words (
site_visits, notsite). - The Guided report and the shared report show
mmm.roifirst when the spec draws it.
Fixed¶
- Score plots of
mmm.decompositionandmmm.waterfallshow one outcome when a score renames the outcome dim to its dataset name. Before, they added all outcomes together. - The Guided what-if and the scenario presets of the several-results template use the spend question, not the results question.
- The mediated template does not ask for channel beliefs, because
mmm.nestedtakes lift tests only. - The data-shape hint of the national templates names the mediator or result columns.
- The
mmm.multivariatestarter spec does not ask for the cross-effects plot when it sets no cross-effects. - A budget plan for one weighted outcome names that outcome and its weight.
- A very long
mmm.*parameter value is cut to 6000 characters, not refused after sampling.
Known gaps¶
mmm.nestedandmmm.multivariateuse national data only and take no control variables.mmm.multivariatehas no lift-test calibration and no promotion modulation.- The Guided template
multi_kpi_mmmsets no weights and no cross-effects. Add them in the spec editor. - The likelihoods binomial, beta-binomial and lognormal are not available.
- A budget plan of a count model (log link) adds the increments of the channels. The removal-based response curves are the correct view of each channel.
- The approximate fits of
mmm.nestedandmmm.multivariatedo not send their loss while they fit.
[0.11.0] — Run tracking — 2026-10-20¶
Run tracking: metrics, parameters, tags and notes¶
Each run now keeps a log of what happened in it: metrics (curves and single values), params, tags, a name, a note and structured log lines. Model code, workflow nodes and you (from a notebook, a script or the command line) can add to this log. You can search runs by their metrics, params and tags. The data that a run comparison needs is stored. The compare page comes in a later release.
Added¶
- A logging API for model code.
FitContext,ScoreContextand the workflowNodeContexthavelog_metric,log_metrics,log_param,log_params,set_tag,set_tags,log_lineandchains. The methods do a check of each key and label when you call them, also without a run. They are thread-safe. The new guide Track metrics, params and tags tells how to use them. - Metric keys keep the pattern
^[a-z][a-z0-9_]{0,63}$. Labels (a maximum of 8) give the qualifiers, for examplelosswith{"split": "val"}orstep_sizewith{"chain": "0"}. mapi_spec.tracking: the key and label rules,TrackingLimits,labels_key,param_textand the run search grammar (parse_run_filter,parse_run_order,parse_run_ref).mapi_sdk.tracking: theTrackerprotocol,MetricRecorder,TeeTracker,with_labelsandspec_params. A fit writes the logged values into the new bundle group/model/metrics_history, andcore.loss_curvedraws them.- Each fit logs the params of its spec:
model,fit.*,data.splitandsettings.*. scikit-learn fits also log the hyperparameters of the estimator asestimator.*. - Live chains: PyMC fits (nutpie and the PyMC NUTS sampler) and
mmm.bayesiansend the progress of each chain. The run page shows one lane for each chain, with its draws, divergences and step size. - Step metrics: nutpie logs
step_size,num_stepsanddivergences. The PyMC NUTS sampler (also inmmm.bayesianwithnuts_sampler: pymc) also logstree_depth. For a chain of more than 5000 draws, it logs each k-th draw. These metrics have the labelchain. ADVI sends itslosswhile it optimises. The evaluation fit ofevaluate_then_refitadds the labelfit=evaluationand the prefixevaluation.to the keys of its params. search.gridlogs each grid point in its search run (the first of the child runs of the node): the paramstrial.<i>.<pointer>andgrid.*, the scores with the point number as the step,best_<metric>and the taggrid.best_point.model.backtestlogs the metrics of each fold with the labelfold.- New endpoints:
PATCH /v1/runs/{run_id}(name, note and tags),GETandPOST /v1/runs/{run_id}/metrics,GET /v1/runs/{run_id}/metrics/{key}(downsampled with LTTB),GETandPOST /v1/runs/{run_id}/params. You can log metrics and params to a run after it finished, for example a business KPI after a deployment. GET /v1/runssearch:parent_run_id,name,tag,filter(for examplemetric.rmse < 0.5orparam.fit.engine = nutpie),order_byandcolumn(Run.columns).- The SSE stream of a run sends
trackingframes when the metrics, the params or the tags change, and asnapshotframe when the name or the note changes. GET /v1/runs/{run_id}/logssends the structureduserlines only withstream=userorinclude_user=true, thus older clients continue to read the logs. The v0.11 client, the CLI and the web app sendinclude_user=true.- The token scope
runs:writeand the permissionupdateonrun(runner role). The audit actionsrun.updateandrun.log. - Python client:
runs.update,runs.log_metric,runs.log_metrics,runs.log_params,runs.set_tags,runs.track(a context manager that sends points in batches),runs.metrics,runs.metric_history,runs.params, and the search arguments ofruns.list. - CLI:
mapi runs metrics,params,log-metric,log-param,tag,renameandnote.mapi runs lshas--filter,--order-by,--column,--tag,--nameand--parent.mapi runs watchshows metrics, params, tags and structured log lines. - Web app, run page: the tabs Overview, Metrics (a chart for each key, colour by a label, label filters, log scale, live updates), Params, Logs (level filter) and Notes (name, tags and note). The runs list shows the name and the tags of each run.
- Budgets for each run (
RunLimits.tracking): 10 000 points for each series, 500 000 points, 1000 series, 5000 params, 100 tags and 10 000 structured log lines. A run that logs more gets oneTRACKING_BUDGETwarning. More than 64 MiB of tracking data is aSANDBOX_VIOLATION. - The setting
MAPI_TRACKING_RETENTION_DAYS. The server deletes the metric points and the structured log lines of old runs. It keeps the summaries, the value of each metric without a step, the params, the tags and the notes. Points that you log later add to the kept summary. - Model Explorer: the backtest has the error for each horizon, pooled over the folds
(
BacktestView.by_horizon), and the chart "Backtest error by horizon". A plan without horizons uses the horizons 1 to the length of the first test window (a maximum of 32).
Changed¶
- Run events: the new types
metrics,paramsandtags. The platform writes them to the tracking tables, not to the event list. Thelogevent has the new streamuser, withlevelandfields.ChainProgresshasstep_sizeandnum_steps. - The legacy
metricevent (for example the holdoutrmseat the end of a fit) also becomes a scalar metric series. GET /v1/runsreturns422 SCHEMA_INVALIDfor afilter,order_byorcolumnthat is not correct.GET /v1/runs/{run_id}/logshas the filterlevel.Runhasname,noteandtags.LogLinehaslevelandfields.mmm.bayesianreports the progress of the sampler from 10% to 90%, and you can cancel it while it samples. A cancelled fit makes no model version.
Known gaps¶
- The page that compares runs comes in v0.14. v0.11 stores the data and gives the summaries and the search filters in the API.
- Runs cannot log images or other media.
- numpyro and blackjax samplers do not send the progress of each chain.
- The approximate methods of mmm-framework (ADVI, Pathfinder) do not send their loss while they fit.
[0.10.0] — Chart wiring and score plots by output shape — 2026-10-20¶
Chart wiring and metric correctness¶
More models now get the plots that apply to them, and the metrics that a spec requests are calculated. A classifier gets classifier plots and metrics. A state model gets a state plot. An ADVI fit shows its loss. The run page shows the plots of a run.
Added¶
- New plot recipes in the core pack, each with an About card and example data:
core.roc_pr: the ROC curve and the precision-recall curve of a 0/1 target, with the AUC and the average precision.core.confusion: the 2 × 2 table of actual and predicted classes at a threshold (0.5, orauto), with accuracy, precision, recall, specificity and F1.core.loss_curve: the loss of each step of a fit, raw and smoothed, with a note when the loss still goes down at the end.core.loo_pit: the LOO-PIT check of calibration and the PSIS Pareto k of each observation. It is not a default plot. Add it in the plot builder.
- Metrics:
auc(the Mann-Whitney AUC) and binarybrier,log_scoreandaccuracyfor a target with two values.masefor a fit with a time holdout: the holdoutmaedivided by the in-sample error of a seasonal naive forecast. metrics.jsonkeeps the PSIS Pareto k of each observation ofelpd_loo(ElpdDetail.pareto_k).- An ADVI fit keeps the loss of each step in the bundle group
fit_history(loss(step), at most 5,000 steps). The fit run addscore.loss_curveto its plots. - Standard plots for more models. The model declares them, and the Insert panel of the spec
editor shows them first. A fit draws only the plots in
outputs.plots:- sklearn regressors (and the quantile, bootstrap and conformal plugins):
core.fit_vs_actual,core.residuals_over_time,ml.feature_importance. sklearn.tabular_classifier:core.calibration,core.roc_pr,core.confusion,ml.feature_importance.ts.gaussian_hmmand the statsmodels Markov switching model:core.state_sequence.- The unobserved components model, ETS and
ts.hsgp_trend:core.decomposition. ETS andts.hsgp_trendkeep a newcomponentsgroup for it.
- sklearn regressors (and the quantile, bootstrap and conformal plugins):
- Default score plots: an areal output gets
sci.area_map, an output of probabilities over astateorregimedim getscore.state_sequence, and an evaluate run with a 0/1 target getscore.calibration,core.roc_prandcore.confusion. ScoreSummaryTable.plots: the ids of the rendered plots that draw the output.- Score, Results by output: a map of areas and a classification view, made from the plots of the run.
- Run detail: a Plots card with the plots of a finished run.
- Compare in the Model Explorer: the card Which version predicts new data best? with the
core.model_comparisonplot, when two or more versions haveelpd_loo(orelpd_waic). - Score, quick chart: a Cell picker for a table with more dims than the axis and the colour, for example a grid. The chart shows one cell at a time.
Changed¶
core.calibrationaccepts one predicted probability for each row (no draws) for a 0/1 target. Validation now accepts it for a model with point predictions.core.state_sequencereads thehidden_statesgroup (HMM), theregimesgroup (Markov switching) and a score summary. It finds the state dim (stateorregime) itself. When the data has more than one series, it shows the first one and names it in the caption.core.decompositionreads thecomponentsgroup. It does not stack a variable with the attrdecomposition: exclude(the trend slope of the UC and ETS models).GET /v1/runs/{run_id}/plotsreturns the plots of the newest attempt that has them. Thus partial runs, and failed or cancelled runs with an earlier partial attempt, also show plots.- One implementation of the MASE scale (
mapi_sdk.metrics.mase_scale) for the SDK, the workflows, the explorer and governance.
Fixed¶
- The fit calculated no metric for a classifier:
metrics.jsonwas empty. The harness did not pair a probability output with its target, because the output declaresunits. It now pairs an output withmeaning: probabilitywith the target. - An evaluate run from the Score page did not draw its plots: the runner skipped
core.fit_vs_actualbecause the result had no observed values. The runner now gives the plots the full predictions and the observed values. aucandmasepassed validation but the fit did not calculate them.- Compare in the Model Explorer showed no holdout metrics of the versions (only the number
of parameters). The explore run did not get the
metrics.jsonof each version. The server now gives it to the run with each bundle. - Score: the quick chart of a large grid table (for example 864 rows of the LGCP summary) looked blank. The chart drew one line through all the rows, so all the cells of a period were at the same x position: each period was a vertical stroke and the band had no width. Empty cells (outside the study area) also broke the line between the periods. The chart now draws one cell of the grid at a time.
Known gaps¶
- Validation refuses
log_scoreinevaluate.metricsfor a model with point predictions, so a point classifier cannot request it. Usebrier. maseis calculated only in the evaluation of a fit. An evaluate run from the Score page skips it, because it does not have the training data.- A regression target that has only the values 0 and 1 gets the classifier plots on evaluate.
- The
sci.*models that run the PyMC engine themselves do not keep the ADVI loss history.PyMCModelplugins andts.gaussian_hmmkeep it. sci.area_mapon the Score page draws an output with only an area dim. An areal output with more dims is skipped with a warning.
Score results by output shape¶
The plots of a score run now follow the shape of each output. A gridded output, for example
the intensity of the earthquake model sci.lgcp_grid, shows as a map with a slider over the
periods of the forecast, not as a forecast fan of the sum over the grid.
Added¶
- The runner picks the default score plots from the shape of the outputs (predict, forecast
and evaluate). A
gridoutput getssci.field_map, orsci.vector_fieldwhen it has acomponentof size 2, with the frame dim (quarter) as the slider. A time series output that is not a grid (total[period]) getscore.ppc_fan. A model with grid outputs and no such time series gets no fan. A model with no grid outputs keeps the plots of before, andjob.plotsstill replaces the defaults. - A predict, forecast or evaluate run of a model with grid outputs writes
ui/fields.json(FieldsView,mapi_spec.views.SCORE_FIELDS_VIEW): the same field view as the Model Explorer, made from the predictions. - The output variables of a grid in
score.zarr.zipcarry the attrgrid(theGridDeclas JSON, with the dataset names of the dims). Thus a download can be plotted as a map. ScoreSummaryTablehas two optional fields:dims(the coordinate columns) andtime_dim(the dim that is time). A server that predates them leaves both out.- Score in the web app shows a view for each output by its shape: a map with a frame
slider for a grid, a line with a band for a time series, intervals for a categorical output
and tiles for a scalar. When
ui/fields.jsonis there, the map replaces thesci.field_mapandsci.vector_fieldplots in the plot list.
Changed¶
- The KPI of a guided forecast (
ui/guided/forecast.json) is an output that is not a grid when there is one, for exampletotaland not the sum ofrateover the grid. - The summary tables are in the order of their size, so a truncated summary drops a large
grid table and keeps
total. - A model's field map (Score page and Model Explorer) colours the 1% to 99% range of the posterior means when the quantiles and draws reach much wider, so a log-scale intensity does not look flat; the mean, quantiles and draws still share the scale, and "Colours for the full range" shows all of it.
Data Explorer for scientific data¶
The Data Explorer now shows graphs of scientific data, not only time series. It finds the structure of the data (images, profiles, maps, networks, trees, directions and points) and shows the sections that apply first.
Added¶
- The explorer finds the shapes of each dataset, and of each node of a tree dataset, from the dims, the coords, the roles and the units. The new section Structure shows the shapes, the role of each dim and how the explorer uses each variable.
- Seven picture sections in the new group Pictures of the data:
- Images and fields: the field viewer of the Model Explorer, with the statistics of each frame.
- Profiles and spectra.
- Map: areas in colour, neighbours and Moran's I.
- Network: matrix, node-link graph, degree and homophily.
- Tree: the tree with the traits and the phylogenetic signal.
- Directions and times of day: roses and the Rayleigh test.
- Points: lattice, CIE Lab colours and agreement.
- The ingest writes
structureand each picture section that applies. The explorer index gives the new fieldsreason,shapesandorder. A request takes the new fieldhintsto correct the structure. - In Expert mode, All series also shows the Plotly plots
sci.area_map,net.graphandsci.rose. - Mock datasets for eleven pymc-challenges cases and the E80 lens tree, and screenshots of
the new sections.
make docs-screenshots-explorersrecords the profiles, the points and the section list from mock mode.make docs-screenshots-sciencerecords the images (E58), the map (E18), the network (E21), the tree (E23) and the roses (E81b) from the real exports. - Images and fields: when a few cells are much brighter than the others (E58 camera frames), the colours cover 98% of the values. Colours for the full range shows all the values. The axis of a histogram with a log count has ticks at the powers of ten.
- Tree: a trait with positive values that go across two orders of magnitude or more (E23 body mass) has a logarithmic axis.
- Each pymc-challenges tutorial has a step "Explore the data". The guide
Explore a dataset (
docs/guides/data-explorer.md) has a new part "Scientific data".
Changed¶
- The list of sections shows only the sections that apply to the data, in the sequence that agrees with the structure. The other sections go in the closed list Not for this data. A link to one of these sections continues to open it. Versions from earlier releases keep the thirteen sections.
- A dim with
policy: extendis time only when its coords are numbers that increase and it is not a list of ids. Aweek,month,quarter,year,day,date,periodortimedim with numbers stays time (an MMM panel on the weeks 1 to 104). A list of ids is a sequence when its name tells a step in time (step,t) or media or spend change along it. If your data uses another list of ids as time, givetime_dimin the request. - Over time adds or averages the cells of an image to make one series. It does not make a series for each cell. The time patterns need a real time dim and 16 periods or more.
- Group comparison compares only real groups. Correlations, Distributions and
Unusual values do not use geometry (borders, neighbours, tree variables, positions).
Data quality gives
SHORT_HISTORYonly for data with a time dim, and has the new warningsGEOMETRY_INVALID,TREE_INVALID,NETWORK_ASYMMETRIC,ANGLE_RANGEandIMAGE_SATURATED. - A request can have all 21 sections (before: 13).
- Map starts with the ratio of the target and the exposure (E18
cases/expected), when the data has this ratio. - Unusual values and the
OUTLIERScheck do not use an indicator (the values 0 and 1, or two values that each occur in 5 % of the rows or more), or a profile or a spectrum (E79reference_dl). One spike in a constant series continues to be an unusual value. - Distributions says "has only the values 0 and 1" for a variable with only 0 and 1. Correlations names the largest pair when no pair has a correlation of 0.7 or more.
- The tables of the picture sections show counts with all their digits (10,292, not 10,290).
- The dataset About counts a pair of dims (
district,district_) once, and does not add an "s" to a dim name that ends in "s" ("186 species", not "186 speciess"). A name that ends in "ss", "us", "x", "ch" or "sh" gets "es" ("3 classes", "3 statuses").
Fixed¶
- The upload page puts the bytes of a file once. Before, Upload and ingest while Preview structure still sent the file failed with "upload failed with HTTP 412".
- Time-series and tabular data keep the classic sections:
- A target named
y, or a pair of columns such assales_xandsales_y, is data, not the positions of points. Points need names of positions and, forxandy, integers, units of length or a dim of points (site). - An MMM panel with integer geo or channel codes is not an image. Over time gives one series for each geo and channel.
hours_open,months_active, a temperature in degrees, and a latitude or a longitude are not directions.
- A target named
- Images and fields reads a few frames at a time. A movie of 1,000 frames of 256 × 256 cells needs about 250 MB more memory, not 1.8 GB. The ingest leaves out the images of more than 50,000,000 cells: Run the deeper checks calculates them.
IMAGE_SATURATEDlooks only for a pile-up at the limit of a detector. A heater setting (E51heater_w) and a noise map (E79noise_sd) do not get it.- Moran's I of a map variable with frames uses the mean over the frames, not the first frame.
- The server does not reuse an explore run from before this release: the cache key of a request includes the version of the explorer.
- Group comparison does not compare the layers of a network (E21
covariate). - The roses show the bin in the units of the data when the pointer is on a bar ("13–14 h", not "203°"). The plain summary of the roses does not count the rose of all the records as a group.
- The export plots of a section have different ids (three roses in E81).
- "has only the values 0 and 1" shows only for a variable with the values 0 and 1, not for any two values between 0 and 1.
- The counts of frames, ties and dim sizes show all their digits. For a version from an earlier release, the header names the views for scientific data apart from the list.
- In Network, the Colour by picker stays when a network has no matrix.
[0.9.1] — Science gallery docs — 2026-10-19¶
This release updates the documentation for v0.9.0. It has no code changes.
Changed¶
- New page "What's new in 0.9", linked from the home page and the nav.
- The README, home page, why, use cases and getting started pages describe the science models, the new plots, the new workflow nodes and recipes, and the pymc-challenges tutorials. The counts of models, plots, recipes and nodes agree with the code.
- The roadmap covers v0.5.1 to v0.9. Plugin upload and the MCP server are "planned"; they are no longer said to come in v0.6. Old M4 to M6 labels for planned work are removed.
- Concepts, guides and reference pages describe circular and grid outputs, custom samplers,
start values, export cases, the new SDK and spec fields, the Score page options, the
scorecard missing-values rule and the
recovery.simulatedcheck. - The conformance guide lists all 26 fixtures. Each science model section in the model reference links to its tutorial.
- Operations pages list the new environment variables and
make docs-screenshots-science. The package READMEs describe the new recipes, nodes and fields.
[0.9.0] — Science gallery — 2026-10-19¶
This release adds eleven science models from the pymc-challenges case studies, for images,
fields, maps and graphs (15 sci.* models in all), and 14 plot recipes for them: maps,
networks, trees, polar charts, image checks, colour swatches and posterior views. New workflow
nodes give posterior summaries, posterior predictive checks, stacking weights, score modes and
plots inside a workflow. Six workflow recipes use them for model ladders, a convergence-gated
Bayesian audit and calibrate-then-design. Each ported case has a tutorial that does the
notebook analysis in the web app, and python -m mapi_plugin_science.export turns the
pymc-challenges data into datasets to upload.
M5 phase 3 — Workflow recipes and tutorials for science models¶
Added¶
sci_spatial_ladderin the workflow gallery. It fitssci.areal_bym2with iid, ICAR and BYM2 area effects and compares them byelpd_looand stacking weights. It keeps the best model and gives the range of the covariate effect. When the chains of the best model agree and the effect is clear, it fits the best model again with wider and narrower priors. It draws a risk map and writes a report.sci_network_ladderin the workflow gallery. It fitssci.network_latent_spacewith covariates, sociality and a latent space and compares them byelpd_looand stacking weights. It gives the pair effects of the best model, a network plot, simulated networks with their statistics and a report.- Two sections in the workflow cookbook, with the documents in
docs/snippets/cookbook_sci_{spatial,network}_ladder.yaml. A test makes sure that each document is the same as its gallery template, and slow tests run the two documents. - Recipe
sci_phylo_signalin the workflow gallery. It fitssci.phylo_regressionwith four residual kinds (no tree, Brownian motion, Pagel's lambda, Ornstein-Uhlenbeck), ranks them byelpd_loo, gives the stacking weights and selects the best. Then it tests if the range of one slope of the winner excludes zero. A clear effect also gets a predictive check. The report has the three tables and thesci.phylogenyplot. On the 186 primates of theprimates301dataset (E23), lambda wins with anelpd_looof 129.9 (the notebook gives 130). - Recipe
sci_calibrate_then_designin the workflow gallery. It fitssci.heat_platewith one calibration run held out, then fits all the runs again. It predicts the field of a new heater layout (model.score,sci.field_map) only if the error on the run held out and R-hat are in their limits. The report has the parameters, the holdout metrics and thesci.image_residualsandsci.field_mapplots. - Both recipes have a section in the workflow cookbook with the CLI and the Python client. Their cookbook documents are tested against the gallery templates and run with the local runner on the simulated data of the models.
model.summaryhas a new parameterfocusand a result fieldfocus. A condition cannot build an entry name from a parameter, sofocusnames one value (beta[north], or onlynorth). The result gives its mean, sd, HDI bounds,p_positiveandexcludes_zero.- Two gallery templates with About cards, cookbook sections
(
docs/snippets/cookbook_<id>.yaml) and run tests with the local runner: bayes_workflow_audit: the checks of a careful Bayesian workflow for each model that gives draws (themodelparameter). The fit keeps prior draws for the prior plot. A gate on R-hat and the effective sample size controls the posterior predictive check, the prior sensitivity check and the recovery check. The report has the prior, pair, predictive and estimate plots.sci_image_model_check: residual maps and a grid of predicted images next to the observed image for an image model (for examplesci.strong_lensorsci.cmb_field), with the posterior predictive check and a report.- Tutorials for the pymc-challenges case studies in
docs/tutorials/pymc-challenges/: an index and one page for each of the 15 ported cases (E07, E18, E19, E21, E23, E31, E50, E51, E58, E60, E61, E77, E79, E80, E81). Each page exports the data, uploads it, builds and fits the spec in the web app, reads the Model Explorer, scores the model and runs the matching recipe, with the CLI and the Python client as tabs. The numbers come from fits of the real data. E51 and E61 use the data that their notebooks simulate.docs/tutorials/complex-workflows.mdshows the patterns of the recipes in the workflow editor: model ladders, gates, predictive checks, governance checks, calibrate-then-design, plots in reports, tree datasets and sub-workflows. The pages are in the navigation and have cards in the tutorials index. - The web mocks (
workflows.gen.json,explorer.gen.json) have the ten gallery templates.
Fixed¶
evaluate.ppcfailed for a model whose output has the name of its canonical target when the dataset binds the target to a different name (for examplesci.phylo_regression, withtraitbound tobrain). The score names outputs in the names of the dataset, and the node now looks for that name.sci.area_mapof a fittedsci.areal_bym2showed no Observed panel: the plot looked forrrin the observed data, and the model keeps the raw SMR assmr. A spec cannot give settings to a plot, so the fit now names the observed counterpart ofrrin the attributemapi_observed, and the plot reads it.- The table of a score result on the Score page lost the cells of a grid when a dim had
numeric labels (for example
y_cmandx_cmofsci.heat_plate, or the lawyer numbers ofsci.network_latent_space). The page took the numeric dims for variables and kept one row for each run. The Arrow views now name their dims in the schema metadatamapi.dims, and the page keeps these columns as keys. - The science tutorials: the build steps now paste the full spec over the starter template (it
has a pipeline, a split and MMM plots), open the closed Datasets section of the
Insert panel, click Create dataset before the upload, take the spec name from
metadata.name, and point to the Events card (the Logs card stays empty). The E18, E21 and E51 tutorials andcomplex-workflows.mdhave eight screenshots from a live dev stack (make docs-screenshots-science). sci_spatial_ladderignored its own convergence check: the branchcleartook the then arm when the chains of the best model did not agree. The branch now needs a convergence check that does not fail, and the report says so.- The scorecard check "Few missing values" (
data.missing) counted each NaN of the raw dataset. Thus it always failed for E18 (the padding of the outlinesring_xandring_y) and E51 (the cells without a thermocouple). The check now counts only the variables that contain observations. It does not count the rolesconstant,indexandmask, or the variables that the model declares in the new class attributeCustomModel.structural_missing(sci.heat_platedeclarestemperature). The expert message names the variables that the check does not count. Gaps in a target, a spend or a covariate count as before, also withmissing: allow. - The gate of
sci_calibrate_then_designstopped the run withEXPRESSION_ERRORwhen R-hat was null (one chain). The condition now usescoalesce, and the recipe needs two or more chains. - The Score page offered No new data for Predict, Simulate and What if. The server refuses such a request when it has no forecast horizon and no scenario dataset ("the score request has no data"). The page now shows No new data only for Forecast, or when a scenario dataset gives the values. For the other modes, a note on the page tells why the option is not there. For a model that has no forecast mode, the note says that the model always needs values to score.
- Some suggested specs of
mapi_plugin_science.exportneeded changes by hand: - E50 lists
elpd_loo, but the spec did not keep the log-likelihood, so the fit did not calculateelpd_loo. The spec now hasfit.keep: {log_likelihood: true}and thesci.profile_bandsplot. - E60 listed
sci.field_map, which cannot draw the vector outputtraction. The spec now listssci.vector_field. It also fits on all of the data (mode: train_only), with no metrics. - E80 listed
sci.field_map, which draws the parametere_lightand not the image. The spec now listssci.image_residualsandsci.ppc_image_grid, and fits on all of the pixels (mode: train_only). - With
--synthetic, the E77 spec held out speakers that the simulated data does not have, and the E79 spec had a bandpower bin that the simulated patch is too small for. These specs now fit the simulated data.
New tests make sure that each suggested spec validates against the schema of its exported
file. A slow test fits each spec on the simulated data with few draws and makes sure that
each plot of the spec shows, that a map shows an output of the model, and that the fit
calculates elpd_loo when the spec lists it.
- The score summaries of a circular output (OutputDecl.circular) were calculated on a line,
not on the circle. For draws on the two sides of north (350 and 10 degrees), the mean was
near 180 degrees and hdi_90 covered almost the full circle. Now the summaries of draws use
the circular mean, and each interval (hdi, eti) and quantile is centred on that mean. The
values are in [0, period), so an interval with lower more than upper crosses the seam.
sd is the circular standard deviation. The difference between a scenario and its baseline
(delta/summary, delta/draws) is wrapped to [-period/2, period/2). This is the same
rule as the evaluate metrics. A circular output that has no valid period attr, or that is
aggregated over a dim, keeps the summary on a line, and the result has a warning.
- ScoreSummaryTable (summary.json) has a new field period for a circular output, so that
a reader knows that the statistics are on the circle.
- sci.circular_regression also declares mean_direction circular, with a period attr.
- governance.check with recovery.simulated gave error for sci.lgcp_grid and
sci.areal_bym2, and skipped for sci.network_latent_space. The check added residuals to
the fitted values, so the simulated counts were not whole numbers and the refits failed. Now
a Bayesian model with predictive draws simulates each dataset from its posterior predictive
(one posterior draw and the noise of the model). The known values are the values of that
draw. A count target without predictive draws gives skipped with the reason. A simulated
dataset that the model cannot fit is counted in the evidence (failed_simulations).
- The governance checks found no predictions for a model whose output has the name of its
canonical target when the dataset binds the target to a different name (tie bound to
cowork in sci.network_latent_space). They now look for the dataset name too.
Known gaps¶
governance.checkwithsensitivity.priorcannot find the prior widths ofsci.areal_bym2(intercept_sd,slope_sd,sigma_scale), so the check is skipped. The spatial ladder fits the model again with these widths in its place.- The graph check of the network ladder plots the statistics of the simulated networks only. The plot does not mark the statistics of the observed network.
core.prior_predictivecannot draw the prior draws ofsci.heat_plate(the draws are on the sensor dim, the observed data is on the grid). Some science models (for examplesci.cmb_fieldandsci.phylo_regression) keep no prior draws. The audit then shows a failed prior plot and continues.- Only
sci_spatial_ladderhas run end to end on a live dev stack and in a browser. The six M5 recipes run through the server orchestrator in a slow test (packages/server/tests/test_workflows_m5_templates.py), with small fits.
M5 phase 2 — Science gallery models¶
Added¶
- Eleven science models in
mapi-plugin-science, ported from the pymc-challenges case studies. Each has an About card, a section indocs/reference/models.md, a conformance fixture, a data export case and tests against the real data of its notebook: sci.heat_plate(E51): the 2-D steady heat equation of a heated plate, a finite-volume system solved in the model graph; predicts the field of new heater layouts.sci.turing_pattern(E61 part A): Schnakenberg rates from early images of a growing Turing pattern, with the exact Fourier-mode likelihood, the dispersion relation, P(Turing) and simulated patterns.sci.storm_localisation(E58): single-molecule localisation in raw EMCCD STORM frames, with camera calibration, spot detection, a batched pixel-integrated Gaussian PSF fit and a super-resolved render kept in the bundle.sci.ibp_features(E31): latent binary features of small images (truncated Indian buffet process with the patterns summed out).sci.colour_naming(E77): colour naming of one World Color Survey language, pooled or with speaker effects.sci.cmb_field(E79): field-level inference of a masked CMB map with a Wandelt-Eriksen Gibbs sampler (sky draws behind the mask and bandpowers).sci.areal_bym2(E18): disease mapping on a graph of areas with iid, ICAR, proper CAR or BYM2 effects.sci.network_latent_space(E21): ties of a graph from pair covariates, sociality and Procrustes-aligned latent positions.sci.graphical_model(E19): a Gaussian graphical model, dense LKJ or horseshoe neighbourhood selection, with edge probabilities.sci.phylo_regression(E23): regression on a phylogeny with iid, Brownian, Pagel lambda or OU residuals.sci.circular_regression(E81): von Mises, projected normal and regime-mixture regression of angles, and log-Fourier activity densities with overlaps.- Export cases e18, e19, e21, e23, e31, e51, e58, e61, e77, e79, e81 (KSBA winds) and e81b (Barro Colorado camera-trap times). A case whose notebook simulates its data (E51, E61) needs no data directory.
OutputDecl.circular(mapi-spec): an output that is an angle (a direction, a time of day). The model sets aperiodattr on the scored array, and the holdout metrics use the circular mean, errors wrapped to half a turn, intervals around the circular mean and an arc-distance CRPS (mape,smape,wapeandr2are skipped).sci.circular_regressiondeclaresdirection_hatcircular.mapi_plugin_science._common: the seeded generators, the quiet PyMC context, the attr cleaning and the draw selection thesci.*modules share (one copy instead of ten).
Changed¶
BindingMaprewrites the dim names in a variable'sgridattr when it renames dims, so thesci.*image recipes find the grid of a score tree after adata.bind.-
EnginePlan(mapi-pymc) hasinitvalsandjitter: a plugin can start every MCMC chain at one given point (pymc,nutpieand the external NUTS bridges).sci.ibp_featuresuses it to avoid a start where all its features switch off. -
The SDK harness parses the model's settings before the data is transformed or split, so a cross-field settings rule (a pydantic
model_validator) refuses the fit early.sci.storm_localisation,sci.graphical_modelandsci.colour_namingcheck their settings-only rules there, as the phase 1 models do.
Fixed¶
python -m mapi_plugin_science.export --listlists every case (a case registered after the__main__guard was missing from the command line).sci.circular_regressionevaluate metrics treated angles as numbers: draws around north (350° and 10°) had a mean near 180°, and the MAE of a good fit was about that of a constant guess. They are circular now (seeOutputDecl.circular).sci.cmb_fieldsimulate drew the mean of each new patch from the vague 100 µK prior of the fitted patch's mean; it now comes from the spectrum.sci.storm_localisation:render_px_nmis at least 1 nm, and the fit refuses (SETTINGS_INVALID) settings whose render ofscore_drawsdraws is over 50 million values (a 0.1 nm render could ask for terabytes).score_bundlerefuses a mode the model does not support (MODE_NOT_SUPPORTED) before any mode-specific work: a forecast of a model with text labels on the horizon dim raised an untypedUFuncTypeError, and some models answeredACTUALS_NOT_SUPPORTED. A horizon along text labels isCOORD_NOT_ALLOWED.sci.graphical_model: score data without acelldim isSCHEMA_INVALID, not aKeyError(USER_CODE_ERROR).- Conformance: e18_areal and e19_ggm pass at
--size full(their score expectations fixed the tiny data's district and protein counts; the hooks now check them against the data).
Known gaps¶
sci.turing_pattern: the notebook-sized fit takes about 5 min and mixes slowly (r_hat about 1.02, bulk ESS about 180 for rho and g).sci.network_latent_space: the latent-space fit with covariates (model c) warns at 4 × 1000 draws (max r_hat 1.08, bulk ESS 46); it needs more draws or tuning.sci.network_latent_space: evaluate metrics include the diagonal (self-tie) cells.
M5 phase 1 — Science gallery foundation¶
Added¶
- Core plot recipes (
mapi_plots.recipes.extras, Vega-Lite):core.pair(corner plot with divergent draws),core.ridge(one density for each level, with HDI),core.rank_matrix(P(rank = k) and P(row above column)),core.calibration(PIT-ECDF difference with a simultaneous envelope, or a reliability diagram for 0/1 targets) andcore.state_sequence(observed series above the state probabilities). - Science plot recipes (Plotly):
sci.area_map(areas as polygons with mean, sd and exceedance panels),sci.phylogeny(a tree with tip estimates),sci.rose(polar rose or density of angles),sci.image_residuals,sci.ppc_image_grid,sci.swatches(sRGB colour squares, also as a map),sci.power_spectrumandsci.point_pattern(points with error ellipses over a field). - Network pack
net(entry pointmapi.plots: net):net.graph, a node-link drawing and an adjacency matrix of an edge variable(…, node, node_). The catalog shows a "Networks" filter. - Each new recipe has an About card, an example in
mapi_plots.examples, a gallery image and a row indocs/reference/plots.md. - The plot sandbox applies the theme to polar subplots (
barpolar,scatterpolar). - Workflow nodes:
model.summary(posterior summaries, R-hat, ESS, divergences; conditions can readnodes.s.params[...]),evaluate.ppc(coverage, PIT KS distance, Bayesian p-values),compare.stacking(stacking and pseudo-BMA+ weights from the pointwise elpd thatmodel.fitnow keeps),model.score(a score mode as a dataset) andplot.render(any catalog recipe as aplotoutput). The new node kind isplot. model.forecastandevaluate.residualsnow give theirplotoutputs.report.summaryhas aplotsinput andplotsections. The web run view draws the plot outputs of nodes and reports.- The SDK
MetricsReporthas anelpdfield with the pointwise values (up to 10 000 points). The server reads it from the fit run of a workflow. model.fitandmodel.backtesttakenodesandjoin, so tree datasets work in workflows. The local runner acceptsxr.DataTreeinputs.mapi_workflows.testing.science_registry()registers the science plugin.- Data export of the science case studies:
python -m mapi_plugin_science.export <case> --challenges <data dir> --out <file.nc>(and--list) writes a file that you can upload. Cases e07, e50, e60 and e80.
Changed¶
mapi-workflows[runtime]depends onmapi-plots[recipes].- The server puts the plot outputs of workflow nodes through the plot sanitizer when it
collects them (an unsafe plot fails its node with
PLOT_UNSAFE) and again when it serves node runs, run outputs and report plot sections. - Workflow validation resolves the
recipeofplot.renderagainst the bundled recipes and the plot catalog (REF_UNRESOLVED). model.summarycomputesmax_rhatandmin_essover all the values of the selected variables (up to 20 000), not only the 200 values of the table.model.score,model.forecastandplot.render(source: predictions) send a tree input whole and give a tree-input model the tree of that input.plot.renderwithsource: datakeeps the groups of a tree input.
Fixed¶
sci.phylogenymatched tips by label when a 0-based tip coord shared a label or two with the node positions by chance; most tips were then empty.sci.image_residualswithlogdivided log10 residuals by a σ in data units. The normalised residual is now in data units.- A non-finite
elpd_loo(a log-likelihood of-inf) made the fitted bundle impossible to load. Such an estimate is now left out, and bundles that already hold one load. - The pseudo-BMA+ bootstrap of
compare.stackingno longer holds a(n_bootstrap, N)matrix (800 MB at the limits).
Known gaps¶
- A backtest of a tree dataset gives the full tree to each fold. A tree-input model that reads its target from the tree can see the test window. The node shows a warning.
search.gridhas nonodesparameter.- The HTML report does not show plot sections. Only the JSON report and the web run view show them.
[0.8.0] — Tree datasets — 2026-10-18¶
This release adds datasets that are trees of groups. A NetCDF-4 or HDF5 file keeps its groups, a raw HDF5 file is mapped to variables after an inspection of its arrays, and a version can combine several files: each file as a node, files stacked along a dim, or files merged on shared coords. A spec selects the nodes that its pipeline reads, and a model can also ask for the whole tree. A guard checks every HDF5 file in the sandbox before it is opened. The catalog also shows each plot recipe as a card with an example picture.
Known gaps¶
- libhdf5 reads a variable-length text attribute whole before the exact length check. The read is bounded by the run budget, and some rare files with such attributes are refused.
- The web HDF5 mapper cannot set the description of a variable or the DimSpec of a coord. Use the Python client or the CLI for these.
- A local
mapi fit --data x.tree.zipreads the file as a plain dataset. Onlymapi checkresolvesdata.nodeslocally. - Workflow nodes that fit again on changed flat data, and the ablations of governance checks, do not give a tree to tree-input models.
- The tree version page shows the preview and profile of the default node only. The Data Explorer shows the other nodes.
- The web flows for trees ran against the mock API only. The real server and runner path ran in the end-to-end test.
Added¶
- Tree datasets (M4). A dataset version can hold an
xr.DataTree(layouttree): one node for each group, with node paths such as/and/instrument/psf. ATreeSchemadescribes it (oneDataSchemafor each node;mapi_spec.tree).DatasetVersionhaslayout,tree_schemaandsources. The version storesdata.tree.zipandtree_schema.json, and itsschema.jsonis the schema of the root node. Migration0014_tree_datasetsadds the columns. Refer to the new guide Tree datasets, HDF5 and multi-file versions (docs/guides/tree-data.md). - NetCDF-4 and HDF5 groups. The ingest opens a NetCDF-4 (or xarray-written HDF5) file with
groups as a tree, and a
.zarr.zipwith nested groups too. Empty groups stay as nodes. - Raw HDF5 files (format
hdf5,.h5/.hdf5/.he5).IngestMapping.hdf5(Hdf5Mapping) tells which arrays become which variables of which node, with their dims (Hdf5Var) and coords (Hdf5Coord: an array of the file, a range of numbers or dates, or an integer index). One warning lists the arrays that the mapping does not name. - Upload inspection.
POST /v1/datasets/{dataset_id}/uploads/{upload_id}/inspectstarts aninspectrun, andGET …/inspectiongives theUploadInspection: the groups and arrays (path, shape, dtype, chunks, filters, dims hint, small attributes) from the metadata only.409 NOT_READYwhile the run is not complete.MAPI_INSPECT_TIMEOUT_Ssets its time limit. - Versions made from several files.
DatasetVersionCreate.assembly(IngestAssembly) combines a maximum of 64 uploads in one ingest run. Each source goes to a node and joins the other sources of that node bynode,concat(along a new dim, one label for each file) ormerge(on shared coords, withjoinexact,innerorouter). data.nodesanddata.joinin specs. A spec selects nodes of a tree (NodeSelect:path,vars,prefix). The platform merges them into the one flat dataset that the pipeline reads. Static validation resolves the merge from thetree_schemaof the version. A side table that is a tree reads one node (side_tables.<name>.node).- Tree-input models.
Capabilities.input = "tree"gives a model the whole bound tree inctx.tree(fit, score, evaluate, the Model Explorer, governance checks and workflow scoring), next to the flat input.mapi_sdk.treehasselect_nodes,infer_tree_schema,validate_treeandread_tree_zip/write_tree_zip. The contract kit hascheck_tree_model,TreeModelFixture,tree_datasetand the reference modeltoy.tree_linear.fit_specandscore_bundleaccept tree datasets. - The Data Explorer shows one node of a tree at a time. Ingest writes the views of each node
under
ui/explore/nodes/, and aDataExploreRequesttakes anode. - Python client:
datasets.uploadtakes anxr.DataTreeand raw HDF5 files;datasets.upload_files(withAssemblyFile),stage_upload,inspect_upload,upload_inspection,inspectandinspect_file. - CLI:
mapi datasets uploadtakes several files with--node,--concat DIM=LABEL,--mergeand--join. New commandsmapi datasets inspectandmapi datasets ingest. - Web app: the upload page takes several files for one version. Each file has a format
(from its extension;
.h5/.hdf5/.he5givehdf5), a node path and a combine rule (node, concat with a dim and a label, or merge with a join). The page checks the assembly and shows every problem before the upload. A raw HDF5 file is inspected first; an array browser (path, shape, dtype, dims, filters, warnings) lets you map arrays to variables, with dims, roles, units, a missing-value rule and a coord for each dim (an array of the file, a start and step, or an index). Each problem shows next to the input it is about, in a live region that a screen reader announces. NetCDF and Zarr files can be inspected to preview their groups. - Web app: the version page of a tree dataset has a Nodes tab (the node tree, the schema
and coords of each node, and the preview of the default node). The Data Explorer takes
?node=and has a node picker. The spec editor completesdata.nodes(node keys, paths andvars) from the tree schema, and the Insert panel has a Tree nodes group. sci.strong_lensdeclarescapabilities.input = "tree". When the tree has a/psfnode, the fit uses its PSF; otherwise the flatpsf, as before. The new settingpsf_source(auto, the default, ortree) lets a spec select only the image node: withtree, the flat input needs nopsfand the fit requires the/psfnode.mapi_plugin_science.lens_data.to_treebuilds the tree form of the lens data.- Plot cards with examples. The catalog's Plots tab shows each plot recipe as a card with
a picture of the plot, a plain-language summary and what the plot needs. A card opens the
plot's full About card (when to use it, how to read it, notes for experts) with a larger,
interactive example. Plots are also in Browse and in
GET /v1/catalog/abouts(kind=plot). - Every built-in recipe has an About card (
mapi_spec.plot_abouts) and synthetic example data, the science pack included.PlotCatalogEntrycarriessummaryandabout; a recipe can set its own with@recipe(..., about=, example=). - A describe run also renders each recipe on its example data (
plot_examples.json). The finalizer sanitizes the examples and stores them with the catalog (migration0013_plot_examples), andGET /v1/catalog/plots/{plot_id}/{version}/exampleserves them. A broken example never fails the describe run. - The docs' plot recipes catalog has a section and a picture for each recipe;
render_gallery.pynow draws every recipe (Plotly through kaleido).
Changed¶
h5pyandh5netcdfare explicit dependencies ofmapi-runner. Before, only the MMM plugin brought them. Each runner image now reads NetCDF-4 and HDF5 uploads.- Format
netcdfnow means a self-describing NetCDF-3 or NetCDF-4 file, or an HDF5 file that xarray or h5netcdf wrote. A NetCDF file that has only a root group still gives a plaindatasetversion, as before. - The ingest run of a multi-file version uses the resource class of the sum of the upload sizes. The uploads of one version are a maximum of 5 GB together.
make devdescribes the catalog again after a code change. In a source checkout (noMAPI_UV_LOCK_PATH), the image digest that keys the catalog also hashespackages/*/srcandplugins/*/src. Before, a change that leftuv.lockalone (a new recipe or About card) kept the old catalog. The images still use the hash of their bakeduv.lock.
Fixed¶
- The ingest ignored the groups of a NetCDF-4 file without a warning. Now each group becomes a node of a tree dataset, and the ingest never drops a group.
- A local
mapi check spec.yaml --data lens.tree.zipvalidatesdata.nodesagainst the tree schema of the local tree dataset. Before, it read only the root and refuseddata.nodes. - The plot sandbox refused every
sci.*plot: its client-side check did not allow Plotly's<extra>tag in hover templates, which the server's sanitizer keeps.
Security¶
- HDF5 guard. Before xarray or h5py opens an HDF5 or NetCDF-4 upload, the runner walks it
with the low-level API of h5py, without a link followed. It refuses with
INGEST_UNSAFE_FILE(422): external and user-defined links, soft links that do not resolve, loop or point to a group, hard-link loops, external raw storage, virtual datasets, filters other than deflate, shuffle, fletcher32, scaleoffset, nbit and lzf, user blocks larger than 64 KiB, too many objects or dims, deep groups, large attributes, arrays that declare more bytes than the run budget, and data types whose size has no limit. The worker runs with an emptyHDF5_PLUGIN_PATH. The guard finds the HDF5 superblock behind a user block at each offset that libhdf5 uses. A file that starts with the NetCDF-3 magic goes only to the NetCDF-3 reader of scipy, which never calls libhdf5. Thus a fake NetCDF-3 header cannot take an HDF5 file past the guard, and a NetCDF-3 file whose values hold the HDF5 signature still ingests. Before the guard reads a variable-length text attribute, the worst case of the read (elements × file size) must fit the budget of the run. - The worker returns the typed refusals of the guard as the error of the run, with the names
from the file only in
untrusted_text.
[0.7.0] — Scientific models — 2026-10-17¶
This release adds scientific models with data on a map, an image or a grid. Four new plugins come from the pymc-challenges case studies: event counts on a map, a reaction-diffusion PDE, cell forces from gel movement and a strong gravitational lens. A model can now declare that an output is a 2-D field. The Model Explorer then shows the field with its uncertainty, and new Plotly recipes draw it. The Score page now guides each request in steps, and a forecast with actuals shows its predictions next to the actuals.
Known gaps¶
- The docs' Score screenshots still show the previous form; regenerate them against a live
stack (
scripts/docs_screenshots.mjs). - The Model Explorer shows the Fields section only for fits made with this release. Fit a model again to see its fields.
- The
sci.*recipes and the field viewer are not in the plot gallery of the docs, which shows only Vega-Lite plots. - The real-data loaders of
mapi-plugin-scienceneed a local copy of pymc-challenges (MAPI_PYMC_CHALLENGES_DATA). The platform does not ingest the.npzfiles directly. - The corpus checks run
e80_lensonly at the tiny size. Nobody ran the full size.
Added¶
- Scientific models (
mapi-plugin-science). Four new plugins from the pymc-challenges case studies. They have data on a map, an image or a grid, and their outputs are fields with a range for each cell: sci.lgcp_grid: event counts on a map over time, with a forecast (E07, Oklahoma earthquakes).sci.fisher_kpp: a reaction-diffusion PDE that the model solves in each step of the fit, with a forecast to later times (E50, scratch assay).sci.traction_force: cell forces from the movement of a gel, with exact draws of the force field (E60).sci.strong_lens: a strong gravitational lens in a telescope image, with the source, the Einstein radius and the critical curves of each draw (E80).
Each plugin has a conformance fixture (e07_lgcp, e50_kpp, e60_traction, e80_lens).
The base-bayes runner images contain them (namespace sci).
- OutputDecl.grid (GridDecl): a model can declare that an output is a 2-D field. The
declaration gives the row and column dims, an optional frame dim, an optional component dim
for a vector field, the colour map, the log scale and the origin.
- Fields in the Model Explorer. A new fields section shows each field output on a canvas.
Select the mean, the sd, mean ÷ sd, the 5% or 95% map, or single draws. Use the frame slider
and the draw player (hypothetical outcome plots). Point to a cell to see its value and its
90% range. Vector fields show arrows.
- sci.* plot recipes (Plotly): sci.field_map, sci.vector_field, sci.profile_bands
and sci.ensemble. A recipe can now return a Plotly figure.
- Two new chooser goals ("Make a map or an image from measurements, with its uncertainty" and
"Fit a physical model to measurements") and a new data shape ("A map, an image or values on a
regular grid").
- Score page: type values instead of uploading a dataset. The page now explains what the
chosen model version needs (what it predicts, the inputs it reads and which of them it needs
on future periods, the fitted date range and every fitted level) and guides the request in
steps: what to do (the version's modes in plain words), how far ahead, the values to score
and what-if changes. Values are typed into a grid that starts with the periods to score
crossed with every fitted level, with paste from a spreadsheet and "fill blanks" for a
column; a forecast says what blank future inputs become. A dataset or no new data are still
there, and the rarely changed settings moved under Advanced. A checklist says what will
be sent and what is missing.
- GET /v1/models/{model_id}/versions/{version}/inputs (ModelInputSchema): the input contract
of a version - the one an endpoint serving it would freeze - and its input table with
?periods=N periods after the fit.
- ScoreCall.inline (ScoreInline): data/scenario as inline XrJSON (up to 5 MB each) on
POST …/versions/{version}:score, checked against the version's inputs and stored as run
inputs exactly like an endpoint's inline payloads. The route gets the endpoint score body cap.
- Predictions against your actuals. A forecast sent actuals returns one-step-ahead predictions on those periods (what the model expected before seeing each actual); the Score page now shows them next to the actuals: the number compared, the typical (MAE) and percentage error, how often the actuals fell inside the band and the average over/under prediction, a chart per series in the plot sandbox (actuals as points, the forecast dashed after the last actual) and a table with the error per period.
ScoreSummary.actuals: the actuals a forecast used, as tables named after the prediction variable they compare with (its coordinate columns, thenactual), in the output's names and original units (raw-stage payloads; none for model-space output).
Fixed¶
- A score payload with one label on an integer dim with a step larger than 1 (for example hours with an 8 h step) no longer fails with a false "gap" error. The validator now puts the payload on the grid of the fit.
- The harness no longer treats a vector field (an output with a
componentdim that itsgriddeclares) as an additive decomposition. - The Score page asked every model for
mean/hdi_90summaries, which quantile, interval and point models refuse (CAPABILITY_UNSUPPORTED). Summaries now default to the model's representation (median/q5/q95for quantiles), only the ones it can give are offered, and draws are off for models without them.
[0.6.0] — Spec builder — 2026-10-17¶
Building a spec no longer means knowing every id and column by heart. The YAML editor now suggests the tenant's real models, steps, datasets, columns and plots as you type, checks each step's and the model's settings against that plugin's own schema, and has an Insert panel to search for anything and add it. Next to the YAML there is a Visual view: the same spec as blocks, with a palette to drag steps, models, datasets and plots from, steps reordered by dragging, and a form for every block. Both views edit the same document, so comments and formatting survive switching between them. Web app only: the API, the spec contract and the runners are unchanged from 0.5.1. Verified with the unit and Playwright smoke suites against the mock API, which now serves the real ModelSpec schema and plugin catalog.
Known gaps¶
- Inside a flow mapping (
with: {...}) completion offers setting names once part of a key is typed; on a line of its own it offers them straight away. - The Visual view edits settings it cannot draw as fields (unions of several shapes, maps of per-channel overrides) as JSON text.
evaluate.metricshas no suggestions: the catalog does not list metrics yet.
Added¶
- Spec editor hints: completions and hover text now come from what the tenant actually has.
model.useandpipeline[].uselist the catalog's models and steps;data.datasetlists datasets;data.bindsuggests the model's canonical names as keys and the dataset's columns as values; each step'swithand the model'ssettingsare checked and completed against that plugin's own settings schema, with dim and variable names the step can see;outputs.plotsandoutputs.derivedlist the model's plots and derived outputs. Values are suggestions only: the editor is no stricter than the server. - Spec editor Insert panel (button and tab): search datasets, models, pipeline steps, columns, plots and derived outputs and add them to the spec. Steps come with their required settings filled in; edits keep the document's comments and formatting.
- Spec editor Visual view, next to YAML (the choice is remembered per browser): the spec
as a stack of blocks (metadata, data, pipeline steps, model, outputs, execution). Drag steps,
models, datasets and plots in from a searchable palette (or pick one and press Add), drag a
step's handle to reorder it (or use its arrows), and open a block to edit it in a form built
from its schema: each step's and the model's own settings, the data binding as canonical
name → dataset column, the split by kind, the fit and execution. Dim and variable names the
step can see are offered as one-click suggestions. Both views edit the same YAML, so
comments and formatting survive; renaming or removing a step keeps the split's
after_stepin step. Problems show on their block and open in the YAML view. - Schema forms render objects with declared properties as collapsible nested sections (opt-in; the workflow inspector is unchanged).
- Mock mode serves the real ModelSpec schema and plugin catalog
(
apps/web/scripts/gen_editor_fixtures.py→src/mocks/editor.gen.json).
[0.5.1] — Forecasts with actuals — 2026-10-17¶
A forecast endpoint can now be given the actual results that arrived since the model was
fitted, together with the dates to forecast. The model updates its state with the actuals
using its fitted parameters (nothing is re-estimated) and forecasts the requested dates from
the last actual. Rows with a target value are actuals; rows with the target blank are the
dates to forecast. All twelve statsmodels models can do this, including models fitted before
this release, without a refit. Models that cannot (the PyMC time-series models, mmm-framework
and the tabular models) refuse with 422 ACTUALS_NOT_SUPPORTED; before, the values were
silently ignored. Google Sheets (an Input sheet), the Python client, the CLI, the web app's
Try it panel and every snippet support it. Verified on a laptop against a live local stack,
like 0.5.0.
Known gaps¶
- Models fitted on
transform.log,transform.box_coxortransform.differenceoutput before 0.5.1 carry no data-space label, so they still accept actuals until they are refitted. New fits are labelled and refuse. There is no replayable log, Box-Cox or difference step with an inverse yet, so such models refuse actuals instead of transforming them. - The pre-run checks see the union of a panel's series. A problem in only one series (for
example a missing driver) is caught in the run: the call returns 200 with
run.probleminstead of a 422. The same applies todataset://payloads whose stored summary predatesDataSummary.observed. - When a split after the entry stage ends the model stage earlier (mmm
train_only), a forecast payload that only covers fitted periods predicts those rows instead of appending the horizon. - VARMAX and DynamicFactor refuse series that end on different dates instead of filtering them. The jointly modelled models' own refusal text is reported as untrusted detail only.
- The "horizon.periods is not used" warning reuses the code
SCHEMA_INVALIDat warning level. - The Guided forecast view and fan chart (
ui/guided/forecast.json,sheets_forecast.gs) do not draw the actuals or the new forecast origin yet. - The Apps Script sends date cells as
yyyy-MM-dd, so time steps shorter than a day lose their hours. - The
integrations-try-it.pngscreenshot does not show the actuals editor yet.
Added¶
- Forecasts with actuals (contract and score harness): a forecast request can carry the
actuals that arrived since the fit (target values for periods after the fitted history, or
revising its last periods) and the dates to forecast (target empty). The model updates its
state with the actuals using its fitted parameters - nothing is re-estimated - and forecasts
from the last actual; when the payload ends with an actual,
horizon.periodsperiods are appended after it. See the new "Forecasts with actuals" concept page. - spec:
mapi_spec.actuals(the rules,ForecastConditioningandSeriesConditioning);Capabilities.conditions_on_actuals; problem codesACTUALS_NOT_SUPPORTEDandACTUALS_INVALID(both 422) withActualsNotSupportedandActualsInvalid;ScoreSummary.conditioning;DataSummary.observed(where targets hold values) and the static checks invalidate_score_request(withlive_capabilities, the plugin build that scores);xrjson.observed_labels(the JSON null check of inline payloads);EndpointSchemaSnapshot.conditions_on_actuals,EndpointSchema.actuals_exampleandexample_actuals_call;Freq.label. - sdk:
score_bundlerefuses actuals for models that cannot use them, appends the horizon, fills empty future drivers byfuture_covariatesfrom the fitted history and the actual periods (mapi_sdk.future.fill_future_rows), checks every series (gaps, drivers) and calls the newCustomModel.forecast_with_actualshook with a typedmapi_sdk.actuals.ForecastActuals(its default refuses). Every forecast result carries the conditioning record (root attributeconditioning).toy.ar1is the reference plugin and the contract kit checksforecast_actuals. - runner: the record is in a score run's
summary.json(so the API answer carries it inline) andresult.json. - statsmodels: all twelve models condition forecasts on actuals with their fitted
parameters -
ts.sarimax,ts.arima,ts.unobserved_components,ts.varmaxandts.dynamic_factorby the Kalman filter (asresults.append(new, refit=False); gaps among the actuals are filtered as missing by the univariate ones),ts.etsby its smoother,ts.markov_switchingby the Hamilton filter,ts.ardl/ts.ecmandts.var/ts.vecmon the supplied lags,ts.thetawith its fitted SES weight, drift and season. Revisions replace fitted values, panels update each series from its own actuals, the fitted noise variance is kept and models already fitted (by a spec or thets_readiness_arimaxworkflow) condition without a refit; the plugin version stays 1.0.0. See "Forecasting from new actuals" on the statsmodels page. - server: endpoint calls with inline data are null-checked for target values
(
DataSummary.observed; the values themselves are never decoded), so actuals a model cannot use are refused with422 ACTUALS_NOT_SUPPORTED, and actuals with a gap or without their drivers with422 ACTUALS_INVALID, before a run starts. Score checks on every path (endpoints, model score, runs, Guided) use the capabilities of the plugin build that will score - the catalog entry of exactly the fitted plugin version - so a model fitted before its plugin learned to condition can do so without a refit.GET /v1/endpoints/{alias}/schemareportssnapshot.conditions_on_actualsfrom that build and returnsactuals_exampleand the newinput_table. Promoting a version that cannot use actuals onto an endpoint whose version can is a breaking change for the endpoint'sschema_changepolicy (mapi_spec.endpoints.capability_breaks; blocked by default). - Input tables (spec):
EndpointSchema.input_tabledescribes an endpoint's inline input as one long table - the coordinate columns (the forecast dim first), the target, the drivers and other inputs, each with what it holds - with the periods after the fit as empty example rows.mapi_spec.xrjson.xrjson_from_rowsturns such a table (records, a DataFrame's rows) into XrJSON andmapi_spec.endpoints.table_forecast_callbuilds the forecast over actual rows and blank-target future rows. - Google Sheets with actuals: the generated Apps Script reads an Input sheet in the
endpoint's input-table layout even when the example call sends no data (a forecast) - actual
rows with the target filled, rows to forecast with it blank, dates as
yyyy-MM-dd- sets the horizon to the blank rows, writes the conditioning note above the tables in Result, writes a refusal's message and suggested action there too, and has a second function that creates the Input sheet. The Python, TypeScript, curl and R snippets of a forecasting endpoint add the call over the periods after the fit, to fill with actuals. New tutorial step "Add your latest actuals and future dates" (docs/snippets/sheets_actuals.gs). - Python client:
Endpoints.forecast(alias, rows, *, actuals=, future=, periods=)andRemoteEndpoint.forecast(sync and async) send actuals and dates to forecast from a DataFrame, row mappings or columns;ScoreResult.conditioning;mapi_client.forecast_call. - CLI:
mapi endpoints forecast ALIAS input.csv(or--actuals,--future,--periods,--dry-run);endpoints scoreprints the conditioning note and the inline summary;endpoints schemashows the input table. - Web: the endpoint's Try it has an editor for actuals and dates to forecast (add a period, paste from a sheet, Use in the call) and shows what the forecast was conditioned on with the first summary tables; the Google Sheets card previews the Input sheet layout of a forecasting endpoint; the model Score page shows the conditioning note.
- Live-stack journey
tests/e2e/test_forecast_actuals_stack.py(make e2e): the airline series through thets_readiness_arimaxworkflow onto an endpoint, then actuals and dates to forecast through the Python client and through the server's generated Apps Script (run in Node, calling the live API with an Input sheet), and the refusal of a model that cannot use actuals (ts.hsgp_trend).
Changed¶
- A
forecastrequest whose payload holds target values is refused withACTUALS_NOT_SUPPORTEDby models that cannot condition on them; such values used to be silently ignored (the forecast came from the end of the fit and the horizon was dropped). - Empty values of supplied drivers on the periods to forecast are now filled by a
future_covariatespolicy other thansupplied; before, only drivers missing from the payload altogether were. - The workflow node
model.forecastsends only the rows of itsdata(future drivers) after the fitted history: workflows that wire the fit's own dataset in forecast from the end of the fit again instead of being refused for "actuals" that are the fitted values. - Actuals that only revise periods inside the fitted history are forecast from the end of the fit: the horizon follows the later of the fit's end and the last actual (the fitted periods in between keep their values), and the conditioning message says the actuals were inside (or overlapped the end of) the fitted history.
- Empty periods among the actuals of a model that filters them as missing need their drivers
too; the refusal (
ACTUALS_INVALID) now names them in its message. - The Apps Script snippet's Input sheet: a variable column the sheet does not have is left out of the call instead of failing the script (the server reports missing required inputs), dates are formatted in the spreadsheet's time zone, two rows with different values for one cell are an error, and a refused call is written into Result before the script throws.
Fixed¶
- Forecasts with actuals never score values a pipeline step filled as actuals: with a
target-imputing step (
core.imputewithffillormean) the blank rows of a payload used to be filled and scored as actuals, so nothing was forecast (or blank future dates were reported as "updated with N actuals"). - A model a workflow fitted on
transform.log,transform.box_coxortransform.differenceoutput (no preprocessing-step equivalent; e.g. the VAR-on-differences branch ofvar_vecm_cointegration) is labelledmapi.io/data-space: transformed(mapi_spec.actuals.DATA_SPACE_LABEL,ScoreMetadata.data_space): it refuses actuals withACTUALS_NOT_SUPPORTEDinstead of reading original-unit values as transformed ones, its endpoints do not offer actuals, and every score warns that its predictions are in the transformed space. - Empty targets inside the fitted range are no longer counted as gaps between actuals, so
separate revisions of fitted periods are accepted for models that need a complete history
(
ts.ets,ts.theta,ts.ardl,ts.ecm, the multivariate models). - A forecast payload with no period after the fit and no actual (no rows - a header-only
Input sheet - or only fitted periods) forecasts
horizon.periodsafter the fit instead of answering with zero forecast periods; the Apps Script and the web editor senddata: nullwhen no row is left. - One actual on an integer time axis with a step above 1 appends the horizon with the fitted step (it was refused with a misleading gap).
predictcalls warn that target values after the fitted history are not used (they were ignored silently).- A payload that lists the dates to forecast is checked for drivers on exactly those dates
(with
future_covariates: suppliedit was refused againsthorizon.periods); inline payloads are null-checked for the drivers a forecast needs, so an empty driver on an actual row (ACTUALS_INVALID) or on a supplied future row (MISSING_FUTURE_COVARIATE) is a422before any run. - Text where a number is needed (
"#N/A", a number stored as text, a cell of spaces) is refused withSCHEMA_INVALIDnaming the variable and the period before a run, instead of failing inside it.xrjson_from_rows/table_forecast_call(the Python client'sforecast) and the Apps Script turn numbers stored as text into numbers and blank text into empty cells. - The Apps Script matches the Input sheet's headers trimmed and ignoring case, and refuses a sheet whose target column it cannot find but which has unknown columns instead of sending a plain forecast without the actuals.
- Timezone-aware dates from a DataFrame are sent as dates (local midnight) or naive UTC, not with an offset; an off-grid time coordinate no longer also reports "start before the fitted range".
- The workflow orchestrator read a dataset node's lineage from the persisted row, which a model node started in the same tick did not see yet.
- Docs: jointly modelled series (VAR, VECM, VARMAX, DynamicFactor) need every series on each
actual period; the Sheets troubleshooting table says which problems are a
422and which a failed run.
[0.5.0] — M3.5: Workflows, governance, explorers, mmm-framework — 2026-10-16¶
Analyses can now be automated and models held to written rules before anyone relies on
them. A workflow chains statistical tests, data processing, train/test plans, searches and
model fits into a versioned, scheduled pipeline; a governance policy says which checks a
model must pass (including null tests that show it beats noise) and whether a second person
must approve it; and the Data and Model Explorers show what is in the data and in the fit.
mmm-framework is now the only marketing-mix engine, with budget optimisation built on its
planning module, and twelve statsmodels time-series models join the catalog. Everything was
verified on a laptop against a live local stack (the GCS shim, embedded Postgres, the
local-process executor); real Google Cloud, Docker and human testers are still to come
(see Known gaps). In summary:
- workflows (
mapi-workflows, migration0011): a declarativeWorkflowSpec(a DAG of 51 typed nodes: statistical tests, transforms, splitters, searches, model fit, backtest, evaluate, compare, select, governance, report,branch.ifwith a safe expression language and plain-language explanations, sub-workflows) run by the server's orchestrator with per-node child runs in the sandbox, server-sent events, caching of unchanged nodes, cost roll-up, cron schedules and new-dataset-version triggers. A gallery of templates: time-series readiness to ARIMAX (seasonal and ordinary differencing decided by ADF, KPSS, Phillips-Perron and Zivot-Andrews with an over-differencing guard, an AIC search of the AR and MA orders, an expanding-window backtest against naive benchmarks, selection and governance), VAR or VECM by the Johansen rank, the governed marketing mix and a forecast bake-off. The web app has a visual workflow builder (React Flow), a live run monitor and Guided recipes. - governance (migration
0012): tenant- and project-scoped, versioned policies over 12 checks (shuffled-driver permutation test, noise-placebo drivers, time-shifted drivers, beats naive, holdout accuracy, residual whiteness, rolling parameter stability, sign constraints, prior sensitivity, parameter recovery, convergence, data quality); a validation state machine; second-person approval by a member with thereviewercapability (never the submitter); a promotion gate (NOT_VALIDATED) that checks the validation covered every gating policy as it stands now; HTML and JSON evidence packs; policy editor and review queue in the web app. - explorers: the Data Explorer (13 sections: profile, distributions, time plots,
missingness, outliers, correlations, prewhitened cross-correlation with the KPI by lag,
ACF/PACF, stationarity, STL and seasonal strength, VIF, group comparisons, data-quality
warnings) and the Model Explorer (parameters, diagnostics, fit, residual tests,
contributions, response curves, IRF/FEVD, forecasts, backtests, side-by-side comparison,
governance evidence, lineage), computed in the sandbox (
explorerun kind). - models:
mmm.bayesianandmmm.frequentistonmmm-framework==1.5.0(national and geo/product panels, MFF ingest with mmm-framework's semantics, business priors, lift-test calibration, response grids, counterfactuals, scorecard) and budget plans throughPOST …:optimize(theoptimizerun kind: allocation, efficient frontier, chance of beating the current plan) in Expert and Guided mode.mapi-plugin-statsmodels:ts.sarimax,arima,var,vecm,varmax,ecm,ardl,ets,unobserved_components,dynamic_factor,markov_switchingandtheta, with future exogenous drivers, quantile intervals, IRF/FEVD and cointegration rank selection. - About cards and Help me choose: every model, step, split, template, workflow node, governance check and dataset carries a plain-language About card (the catalog refuses items without one); a chooser asks a few questions and ranks what fits.
- client, CLI and docs: client calls and
mapi workflows,mapi governance,mapi validations,mapi explore,mapi optimize,mapi chooseandmapi about; the v0.5 documentation site (workflows, governance, null tests, explorers, mmm-framework, the workflow cookbook, generated node and check references); a generatedTHIRD-PARTY-NOTICES.mdshipped in every image. - removed (breaking): Atlas (
mapi_client.atlas), BayesInsight (importer, plugin, ingest preset, route),mmm.hierarchical_geoand the pymc-marketing adapter; see Removed below.
The M3.5 review found 46 defects (conformance and UX, security, statistics, reliability); all were fixed before the release, each with a regression test (the Fixed sections below).
Added¶
- spec: the M3.5 contracts for workflows, governance, explorers, budget optimisation and
About cards, shared by the server, runner, plugin, web and client components. The contract
version is now
0.2(the removals below are breaking). mapi_spec.aboutandmapi_spec.abouts: the plain-languageAboutcard (summary, use and avoid when, data needs, assumptions, outputs, difficulty, typical runtime, example, related items, expert notes) on model manifests, step, split and plot catalog entries, guided and workflow templates, workflow node kinds and implementations, governance checks, andDatasetAboutfor datasets (the owner's description plus a summary generated from the schema); the catalog rule (ABOUT_INCOMPLETE) and cards for every built-in step, split, model (including the mmm-framework and statsmodels ids of v0.5) and template; the "Help me choose" questions and the deterministicrecommendranking.mapi_spec.workflow,.workflow_nodes,.workflow_templates: versionedWorkflowSpecDAGs of typed nodes (test, transform, branch, split, search, model, evaluate, compare, select, governance, report, subworkflow) with typed ports, parameters, alternatives and branch gating; static validation with JSON pointers (types, references, parameters, cycles, unreachable nodes); the node catalog (51 implementations with parameter schemas, result fields and About cards); runs, node runs, branch decisions, cache keys, cron schedules, dataset triggers and events; four gallery templates including the canonical readiness-to-ARIMAX workflow.mapi_spec.expr: the safe condition language of branches (a hand-written parser and an evaluator over JSON with hard limits; nevereval).mapi_spec.cron: schedules.mapi_spec.splitplan: multi-fold split plans (holdout, rolling origin, expanding window, blocked and purged k-fold, geo holdout, nested) with pure fold computation that matches scikit-learn's splitters.mapi_spec.stattests: test results and decisions.mapi_spec.governance: versioned tenant and project policies, the twelve-check catalog (null tests against shuffled targets, noise drivers and time-shifted placebos, naive benchmarks, residual whiteness, stability, signs, prior sensitivity, simulated recovery, convergence, holdout accuracy, data quality), evidence, the pure validation state machine, approvals by a second person with the reviewer capability, and the evidence pack. The promotion gate refuses unvalidated versions when a policy says so (NOT_VALIDATED).- Data Explorer and Model Explorer views (
mapi_spec.views) and their requests (mapi_spec.explore);OptimizeRequest/OptimizeResultforPOST …:optimizemapped onto mmm-framework planning (mapi_spec.optimize). RunKindworkflow(a parent),workflow_nodeandexplore;M35_ENDPOINTS(60 operations) merged intoENDPOINTS, with access rules; resourcesworkflow,policy,tenant_policy, actionsvalidateandapprove, thereviewercapability on memberships; scopesworkflows:write,workflows:run,models:validate,policies:manage(no token can approve); audit actions, webhook events and problem codes for all of it.- sdk:
Step.aboutandCustomModel.about/family, and the contract kit's About check (check_about, and anaboutcheck incheck_step/check_model). - plugins:
mapi-plugin-statsmodels(Apache-2.0; statsmodels is BSD-3-Clause), twelvets.*models:ts.sarimax(ARIMAX/SARIMAX),ts.arima,ts.unobserved_components,ts.ets,ts.theta,ts.ardl,ts.ecm(UECM with the Pesaran-Shin-Smith bounds test),ts.var,ts.vecm(Johansen rank selection),ts.varmax(VARX),ts.dynamic_factorandts.markov_switching. Panels loop oversettings.series_dims; outputs are prediction-interval quantiles; forecasts need future drivers (MISSING_FUTURE_COVARIATE). Bundles hold parameters and data only - no pickle - and scoring rebuilds the results (Kalman filter/smoother with the stored parameters, or deterministic re-estimation checked against them). Fits store parameter tables with standard errors and p-values, fit statistics, residual tests, impulse responses with bands, variance decompositions, Granger tests, the Johansen table, ECM bounds tests, smoothed components, factors and regime probabilities;explorer_views()turns them into Model Explorer sections. Reference-value tests against direct statsmodels calls and the statsmodels VAR documentation; conformance fixturessm_arimaxandsm_vecm_macro. The plugin ships inmapi/base-forecast. - docs: Workflows, Governance and About cards concept pages; the v0.5 section of the HTTP API reference; the statsmodels time-series models page.
- plugins/mmm: mmm-framework (
mmm-framework==1.5.0) is the MMM engine.mmm.bayesian(mmm-framework'sBayesianMMM: national, or partially pooled over geos and products; NUTS on nutpie, PyMC or NumPyro, or MAP/ADVI/Pathfinder/Laplace/SMC) andmmm.frequentist(its ridge estimator with bootstrap intervals). Both own scaling, carry-over and units, score by replaying the fitted model per draw in NumPy (checked against mmm-framework's forecaster), prepend the fitted history so carry-over reaches scored periods, follownew_levels(pooled,hyperprior,zero,error) for unseen geos/products, and cache contributions, response curves and marginal costs. Business priors translate in closed form into log-normal ROI priors (and an intercept prior solved against mmm-framework's own baseline prior); lift tests become mmm-framework experiment likelihoods (marginal ROAS or contribution). MFF panels are handed over in the layoutMFFLoaderbuilds. - runner, server, sdk: the
optimizerun kind andPOST /v1/models/{model_id}/versions/{version}:optimizewithGET /v1/optimizations/{run_id}(Prefer: wait,Idempotency-Key, anoptimize.completedwebhook): budgets split across channels (or geo x channel) by mmm-framework's planning module from the posterior's response curves, with bounds, groups, risk objectives, a horizon and the efficient frontier; themmm.budget_plansplot.CustomModel.optimizeandmapi_sdk.models.BudgetPlanare the plugin hook. - conformance:
e33andguided_mmmfitmmm.bayesian; themmm_frequentistfixture. - docs: "Marketing-mix models on mmm-framework" (data contract, how the spec maps onto mmm-framework, priors, lift tests, optimisation, limits), the rewritten marketing-mix guide and the "Optimise a budget" tutorial.
- workflows (new distribution
mapi-workflows): the workflow engine and its nodes. - A JSON-only engine (
mapi_workflows.engine) that both the control plane and a local runner drive: topological scheduling withmax_parallel, branches evaluated with the safe expression language, input alternatives, skips, retries,on_failure: continue, cancellation, deadlines, cache keys and an event log; orchestrator nodes (compare.models,select.best,select.threshold,report.summary,report.evidence_pack) computed on JSON results only. - The data nodes (
mapi_workflows.runtime, extraruntime): 17 statistical tests checked against statsmodels, scipy and arch reference values (Phillips-Perron included), transforms, the seven train/test plans, ARMA/SARIMA/VAR-lag/cointegration-rank searches, grid searches, backtests (window plans, masked k-fold, geo hold-outs), forecasts, benchmarks, metrics, residual diagnostics, Diebold-Mariano with the HLN correction and governance nodes over pluggable check runners (mapi.governance_checks). LocalRunner: runs a workflow in-process (threads, timeouts, cancellation, an in-memory cache, sub-workflows); the canonicalts_readiness_arimaxtemplate runs end to end.- The generated node reference (
docs/reference/workflow-nodes.md). - runner: the
workflow_noderun kind executes one node in the sandbox, with model inputs loaded from their bundles. - server: workflows (migration
0011_workflows, seven tables under forced row-level security): documents and revisions validated at every save, runs asworkflowparent runs driven by the orchestrator's workflow loop, nodes asworkflow_node,fitand child workflow runs, derived datasets registered aswf-<workflow>-<node>versions, cache reuse across runs, cancellation through the run state machine, SSE events, output links, cost roll-up, cron schedules,dataset.version.createdtriggers with debounce, webhooks and audit; the dispatcher and reconciler skip parent runs. - docs: the workflows guide (
docs/guides/workflows.md) with the canonical document and a cookbook whose fragments are validated by a test. - Governance (server, runner): policies, validations and approvals end to end.
- Every check of the catalog runs in a sandboxed
governancerun (new run kind,GovernanceJob→governance.json), model-family aware (Bayesian, frequentist, machine learning) under a refit budget: block-permutation null test, placebo noise drivers (new channels or variables, measured by ablation through the score path), time-shift placebos, naive and seasonal-naive benchmarks out of sample, Ljung-Box whiteness, rolling-window stability, effect signs, prior sensitivity, parametric-bootstrap recovery, convergence, holdout accuracy and data quality, each withmessage_plain, asuggested_action(new onCheckResult) and evidence. - Migration
0012_governance: versionedgovernance_policies,model_validationsandvalidation_approvals(forced row-level security) andmemberships.capabilities. - Policy CRUD for projects and tenants with versions and retirement; the platform's
default policies per model family (
DEFAULT_POLICIES, seeded on a tenant's first use; they never gate promotion by themselves); validation requests, resubmission, cancel and deprecation; second-person approval with thereviewercapability (never the submitter, never an API token); the promotion gate refuses unvalidated versions withNOT_VALIDATEDwhen a policy requires it (audited asendpoint.promote_not_validated); evidence packs as JSON and self-contained HTML; audit rows and webhook events for every step. - Explorers (runner, plots-core, server): the 13 Data Explorer sections (overview,
distributions, time series, missingness, outliers, correlation, cross-correlation, ACF/PACF,
stationarity, STL, VIF, group comparisons, quality warnings) and the 11 Model Explorer
sections (parameters, diagnostics, fit vs actual, residual tests, decomposition, response
curves, IRF, FEVD, forecasts, backtests, comparisons) computed in the sandbox: the quick
data sections at every ingest, the model sections at every fit, the rest in
exploreruns (POST …/explore, cached per request).POST /v1/models:comparecompares several versions with Diebold-Mariano tests. Vega-Lite recipes for every view inmapi_plots.explorer(JSON-only). Dataset About (GET/PATCH …/about). Statistical helpers are validated against statsmodels/scipy reference values. - docs: guides "Validate models with governance policies", "Explore a dataset" and "Explore a model".
- integration (M3.5 parts working together):
- Workflow model nodes fit the real models:
mapi_workflows.testing.test_registry()registers the statsmodels plugin (extramapi-workflows[testing]), and the canonical readiness -> ARIMAX workflow runs end to end on the realts.sarimax(in-process, through the server and on the real runner). Model nodes bind a model's target, other roles, time dim and categorical dims (a panel'sgeo, a multivariate model'svariable) to the data automatically, and rewrite carried transform steps and holdout splits in the model's names. Multivariate nodes read the series along the data's series dimension (the layout ofts.var/ts.vecm);test.vifexpands a channel dimension into columns. Themmm_governedtemplate fitsmmm.bayesianon a geo panel. - Governance inside workflows:
mapi-runnerexports every governance check as a workflow check runner (entry pointmapi.governance_checks), sogovernance.policyandgovernance.checknodes measure models with the same family-aware implementations as a validation request. In the server the policies in force come from the policy store, andsubmit: trueon a passing node records the evidence as a validation (validated, orawaiting_approvalwhen a policy wants a reviewer). - Checks and explorers on every family (statsmodels, mmm-framework Bayesian and
frequentist, PyMC time series, tabular, scikit-learn, choice), tested on every
conformance fixture: the Model Explorer prefers a plugin's own views
(
explorer_views, e.g. the statsmodels IRF/FEVD and test tables); predictions are compared with the observed target at the model's stage (pipelines that resample or convert currencies); score requests name the spec's supplied side tables. - Catalog About cards and Help me choose are served:
GET /v1/catalog/abouts,GET /v1/catalog/chooserandGET /v1/catalog/recommendations; every operation of the endpoint table is now served. About cards for the five scikit-learn models; a test holds every selectable item of the installed catalog (models, steps, splits, guided and workflow templates, workflow node kinds and nodes, governance checks) to a complete card. - Images assert the workflow runtime, statsmodels and scipy in the runner images (JAX
with mmm-framework in
base-bayes) and the JSON-only workflow engine in the server image;packages/sdkjoins the strict mypy list. - web: About cards, Help me choose, the explorers and budget optimisation.
- About cards (plain language first, expert notes and references collapsed) in the catalog's new Browse tab (every selectable item, filtered by kind, difficulty and text), the model and step details, the spec editor (an About tab, a button on every pipeline step) and the Guided template cards; dataset About (the owner's description, editable, next to the generated summary) on the dataset page and the Data Explorer.
- Help me choose (
/choose, and on Home and at the top of Guided mode): one question at a time, live ranked matches with reasons and cautions, and Start with this into a Guided analysis, a new spec with the model, or a workflow template. - The Data Explorer of a dataset version: all thirteen sections from the ingest's views and explore runs (polled, cached, kept in the URL), with variable and series pickers, interactive Vega-Lite plots in the plot sandbox, plain readings first in Guided mode and every statistic (and the platform's export-ready plots) in Expert mode.
- The Model Explorer of a model version: parameters (sortable, with intervals),
diagnostics, fit vs actual, residual analysis, contributions, response curves, IRF and
FEVD, forecasts and backtests on demand, governance evidence with the validation
status, and lineage; Compare (
POST /v1/models:compare) with Diebold-Mariano tests. - Optimise budget for marketing-mix models (
POST …:optimize): budget, caution, per-channel bounds in multiples or money, groups and the frontier, with impossible bounds explained before the run; the plan with its allocation, expected result, chance of beating today's plan and the efficient frontier; Guided What if… leads to it. - Mock-mode fixtures computed by the real runner and plots-core
(
apps/web/scripts/gen_explorer_fixtures.py,--checkin CI) and generated TypeScript types of the explorer views; the guides' screenshots (make docs-screenshots-explorers). - web: workflows and governance screens (both personas).
- Workflows: the list and the template gallery with About cards; a "use this template"
wizard (name, parameter defaults, bound data); a Guided "run this recipe on my data"
flow. The visual editor (React Flow 12.12.0, MIT, lazy-loaded with the editor and
the run graph) has a node palette grouped by kind with About cards, typed ports that
refuse an incompatible edge with the reason, branch arms that gate nodes, a node editor
whose settings form is generated from each node's JSON Schema (values, workflow
parameters or expressions) with catalog pickers for models, checks and sub-workflows,
a branch condition editor checked live by the server (never evaluated in the
browser), and a YAML/JSON view kept in sync. Problems are pinned to nodes by their
JSON pointers; revisions are saved with
expected_revisionand compared side by side. - The run monitor follows the SSE stream with
Last-Event-IDresume and shows the graph by status, the steps in plain words, decisions with the values they read, outputs and events, cancel and cost. A node drawer shows test verdicts, cached-vs-ran, outputs (tables, scores, folds, reports, evidence, charts in the plot sandbox, file downloads), child runs and logs. Schedules use a cron editor with presets and a preview of the next firing times that matchesmapi_spec.cron; triggers fire on new dataset versions. - Governance: the policy editor (project and tenant-wide; covers, checks with About
cards, severity, limits checked like the server, parameters; approval and promotion
switches; versions with a diff; retire), the check catalog, validations with the
status path and history, per-check evidence with plain messages, what to do and the
shuffled-result distribution, approve/reject (never the submitter; hidden without the
reviewer capability), resubmit, cancel, deprecate and the evidence-pack export. Model
versions have a Validation page, Members a reviewer column, and the promotion dialog
explains a
NOT_VALIDATEDrefusal with a link to the validation. - Mock API (
--mode mock, tests): workflows, runs replayed live, schedules, triggers, policies, validations and approvals.apps/web/scripts/gen_workflow_fixtures.pywrites their data frommapi_specand a real local run of the canonical template and two governed fits (checked bymake web-checkand CI). - web: the M3.5 screens work as one app.
- Navigation per persona: Guided mode lists Guided, Help me choose and Recipes (the workflow gallery as recipes) first, as does a Guided-locked project; Expert mode has Workflows, Governance and Help me choose (G O).
- Start with this opens a workflow template as a recipe (Guided) or in the template wizard (Expert), and a governance check in the check catalog.
- A finished workflow run links each model it fitted to the Model Explorer and the version's validations (run summary, fit step, outputs); a workflow run's run page opens its monitor, and lineage links the workflow run that fitted a version.
- The Model Explorer shows the validation status in its header (a link to the version's validations) and links the latest validation and the policies; a validation links the Model Explorer and the run that computed its checks. Explorer and governance screens share one validation cache and badge.
- The Data Explorer offers the readiness workflow with the data bound (
?data=on the recipe and template wizard pages). - One About card component for every screen (framed or plain, compact for side panels).
- Playwright journeys: Data Explorer → recipe run → Model Explorer → validations, and Guided what-if → budget optimisation and back.
- client (v0.5 calls): a request builder for every M3.5 operation of
mapi_spec.api.ENDPOINTS(checked against the contract; the strict coverage test is no longer an expected failure) and sync and async resources:Client.workflows(documents from aWorkflowSpec, a mapping, YAML text or a file;upsertbymetadata.namethat adds a revision only when the document's hash changed; runs withwait;stream_events, an SSE iterator ofWorkflowSnapshot,WorkflowEventFrameandWorkflowEndthat resumes withLast-Event-IDand never repeats an event; node outputs downloaded and CRC32C-verified; schedules, triggers and the template gallery),Client.governance(the check catalog, project and tenant-wide policies - a fetched policy can be edited and sent back as it is),Client.validations(requests that wait for the checks, the review queue, evidence, resubmission,approve/rejectnever resent after a lost response, evidence packs as HTML or JSON),Client.explorations, and oncatalog(abouts,about,chooser,recommendwith the answers checked before sending, workflow nodes, governance checks),datasets(about,describe,explore) andmodels(explore,compare,optimizewith the keyedPrefer: waitlong-poll,validate). Failed workflow runs raise the failing node's error. Tested with respx and against the real server (embedded Postgres, the storage front, children executed in-process). - cli:
mapi workflows(initfrom a gallery template,validatea file on the server,apply,create,runwith--watch- a live node-status board fed by the SSE stream, exit status 1 when the run fails -watch,status,runs,nodes,node,output,cancel,archive,rm,nodes-catalog,schedules,triggers),mapi governance(checks with their About cards, policies per project or tenant with versions and retirement),mapi validate model(waits and prints every check in plain words with the suggested action) andvalidate workflow,mapi validations(review queue, evidence, resubmit, deprecate, evidence pack),mapi approve(--rejectwith a comment),mapi explore dataset|model|compare|get(views written as JSON),mapi optimize(the plan, the uplift and the frontier),mapi choose(Help me choose in the terminal, or--answerfor scripts) andmapi about. Tested with respx and against a real server over HTTP. - docs: "What's new in 0.5"; the workflow cookbook (the canonical unit roots ->
differencing -> AR/MA order search -> rolling-origin backtest -> ARIMAX vs naive ->
select -> governance document in full, with the client and CLI commands, plus VAR/VECM
cointegration and a forecast bake-off); "Can you trust this model?" (governance and null
tests for non-technical readers and for statisticians); "Choosing a model" (About cards
and the chooser); one section per statsmodels model (About card, minimal spec, forecast
with future drivers); budget optimisation end to end with the client; the client and CLI
guides and reference for v0.5; a home page that leads with the new capabilities; the
roadmap moves plugin upload and MCP to v0.6. The cookbook documents validate and run with
the local runner, the client walkthrough runs unchanged against a real server, every
command in the CLI walkthrough is checked against the CLI, and the statsmodels page quotes
the catalog's About cards (
conformance/tests/test_docs_v05.py,packages/server/tests/test_docs_client_v05.py). - tests, docs: the M3.5 journeys through the running stack (
make dev, thenmake e2e):tests/e2e/test_m35_stack.py(the canonical readiness workflow on a series with a unit root, governance null tests that block a model fitted on noise and a second-person approval, mmm-framework with its explorer views and a budget plan, the Data Explorer, a VAR's impulse responses, Help me choose, andmapi workflows run --watchon the cookbook document) andapps/web/e2e/m35-live.e2e.spec.ts(the same screens in the browser, Guided and Expert, seeded bytests/e2e/m35_browser_seed.py);docs/operations/m35-verification.mdrecords what they verified, the defects they found and what remains. The M1 and M3 journeys were brought up to date with v0.5 (the E33 spec on mmm-framework, the plan's KPI name, webhook deliveries verified as they arrive). - docs: the site for v0.5. The home page shows the new screens at work and leads its
measurements with the M3.5 journeys; "Why modeling-api?" covers workflows, governance,
About cards and the Data Explorer and compares workflows with general orchestrators; "Use
cases" adds a governed forecasting pipeline and macro/VAR analysis; the roadmap marks v0.5
available (with its own page) and M4 next (plugin upload for allow-listed experts, an MCP
server). Screenshots of the workflow gallery, editor, run monitor, a step's drawer,
decisions, schedules, the Guided recipe, policies, the policy editor, the review queue, a
validation and its evidence (
make docs-screenshots-workflows,apps/web/e2e/workflow-screenshots.spec.ts). Terminal outputs on the What's new, cookbook, choosing, Python client, CLI, mmm-framework and budget pages are captured from a live stack instead of illustrated. A generated governance-check reference (reference/generated/governance-checks.md, every check with its About card, limits and parameters, and the default policies); the permissions reference groups each resource's actions, marksapproveas needing thereviewercapability, lists every scope including the v0.5 ones and has a Capabilities section; Tenancy and authentication lists the v0.5 roles, scopes and capability.
Changed¶
- spec: a dataset's generated summary shows dates without a midnight time ("from 2023-01-01", not "from 2023-01-01T00:00:00").
- workflows: the
var_vecm_cointegrationtemplate (andtest.johansen,search.var_lag_order,search.coint_rank) take the series along the data's series dimension;varsis optional (labels or variable names) and the template'sseriesparameter was dropped. -
governance: whiteness leaves out the start-up residuals of differencing; driver effects fall back to the coefficient for models whose in-sample predictions follow the stored history (statsmodels); recovery is skipped for autoregressive models; parameter stability is relative to the estimate's own uncertainty near zero and tolerates windows the model cannot be refitted on; checks that compare numeric predictions skip choice models instead of failing.
-
spec:
workflow_run_eventsdeclares theafterandlimitquery parameters the server already serves for the JSON page of events.
Removed¶
- spec: BayesInsight (
mapi_spec.bayesinsight,mapi_spec.bayesinsight_model, thebayesinsightingest preset withBayesInsightMetadata/BayesInsightModelDef,BayesInsightImport,draft_from_bayesinsight,POST /v1/guided/import/bayesinsight) and theatlassnippet language. Thegeo_mmmguided template (2.0.0) targetsmmm.bayesian(mmm-framework). mmm-framework is the only MMM engine. - server, runner, client, plugins: the BayesInsight import route, ingest preset, client
calls and the
mmm.bayesinsightplugin; the "Import a BayesInsight model" tutorial. - plugins/mmm (breaking):
mmm.hierarchical_geoand the pymc-marketing adaptermmm.pymc_marketing(and thepymc-marketingdependency); specs move tommm.bayesianand drop their scaling andcore.lag_tensorsteps. Thepymc_marketingconformance fixture is nowmmm_frequentist. - client, web, docs, images (breaking): Atlas.
mapi_client.atlas(RemoteModel) and theatlasextra, the Atlas snippet tab, the "Optimise a budget with Atlas" tutorial and snippet, the Makefile's Atlas targets and the e2e Atlas step; budgets are optimised withPOST …:optimize. Thebase-bayesandbase-forecastimages warm mmm-framework instead of pymc-marketing.
Fixed¶
- web, workflows, spec (M3.5 review): the run monitor explained branch decisions only as
raw expressions and "then/else arm" ("Decisions: Difference? -> then", "Took the then
arm") and named the winner by its node id ("backtest has the best average error", "Best:
backtest").
branch.ifnow takes plainexplain_then/explain_elsetexts, recorded asBranchDecision.explain_plainand set in every gallery template; the report summary, the Steps table and the Decisions tab lead with them ("The tests found that the series wanders ..., so it was differenced"), list the verdicts of the tests a condition read, and keep the expression under For experts. Steps not needed say why in the same words, and selections and comparisons name candidates by their step labels ("ARIMAX", "Naive forecast"); the gallery templates label their candidates. - web (M3.5 review): the budget plan showed Guided users the planning library's own wording ("Goal: expected KPI (posterior mean)", "the greedy allocator's exactness precondition fails", "posterior interval", "Marginal ROI"), and its result tiles read "90% likely 1.3M – 1.3M". In Guided mode the goal and the known notes are now plain sentences, the original wording is under For experts, and the tiles use as many digits as their ranges need ("1.305M – 1.312M").
- web (M3.5 review): Help me choose scrolled sideways on a 375 px phone: its five suggestion tabs were wider than the screen and "Where to start" wrapped onto three lines. A switch with more options than fit now scrolls inside its own strip, on every page.
- web (M3.5 review): Start with this on a model in Help me choose or the catalog
dropped a Guided user into the Expert YAML spec editor, with placeholder data and an
error badge. In Guided mode a model now starts the guided template built on it
(
tabular.regressor→ store demand,mmm.bayesian→ geo marketing mix, ...); a model with none says it needs Expert mode and switches only on a click (never in a project locked to Guided mode). - licensing, images:
LICENSE.mdand the licensing page now listpackages/workflows(Apache-2.0) with the other Apache-2.0 components. The licensing page now names the third-party libraries v0.5 added: statsmodels (BSD-3-Clause), NumPyro and JAX (Apache-2.0) and nutpie (MIT), which mmm-framework brings in, and @xyflow/react (MIT). A newTHIRD-PARTY-NOTICES.md, generated fromuv.lockandpnpm-lock.yamlbymake notices, lists every Python and JavaScript package the images ship with its version and licence. It reproduces the NOTICE files and the licence texts of the JavaScript packages compiled into the web app. Every image now carries it, with the licence files, in/usr/share/doc/mapi/.make release-checkand the test suite fail when it is stale. - spec, server (found by the M3.5 live-stack verification): a governance policy with
applies_to: {families: [ts]}- the example of every governance page - covered none of the statsmodels models, which declare the familyforecast,ts_multivariateorregime; its checks, approval and promotion gate silently never applied. A family now matches a model's declared family or the namespace of its id (tsforts.sarimax). - server (found by the M3.5 live-stack verification): uploading data that was uploaded
before - the same file as the next version, or into another dataset or project - failed
its ingest with
INTEGRITY_FAILURE("ui/explore/index.json already holds different content"). Ingest outputs are stored under the data's content hash, and the v0.5 Data Explorer views written at ingest name the dataset version and carry plot timestamps. Each ingest's views are now stored under its run (t/<tenant>/datasets/<sha>/runs/<run>/ui/explore/); the data and its sidecars keep the strict same-content check. - client, cli (found by the M3.5 live-stack verification):
explorations.view(andmapi explore --show) returned a section's plot list instead of its view for every section whose name sorts afterplots(residuals,response_curves,stationarity,stl, ...):ui/explore/plots/<section>.jsonmatched first, and the call failed with "the view is not a JSON object". The exact view is now looked up first; plots never match. - spec (found by the M3.5 live-stack verification): Help me choose ranked
ts.sarimaxsixth for "forecast weekly sales with drivers" although it is the only model that fits all four answers: items silent on a question scored the same 1.0 and ties broke on difficulty. Ties now break on the number of answers an item fits explicitly first. - web (found by the M3.5 live-stack verification): on the dev server (
make dev), the first page to open a workflow graph crashed with "Invalid hook call" inReactFlowProvider, and a lazy route loading while the editor's YAML worker first started failed with "Failed to fetch dynamically imported module": Vite discovered React Flow and the monaco-yaml worker's dependency late, optimised them in a second pass and reloaded. Both are now pre-bundled at start-up (apps/web/dev-prebundle.ts, with a test that every package a web worker imports is listed); production builds were not affected. - runner (found by the M3.5 live-stack verification): the Data Explorer's overview read
"2 variables over 156 time" for the most common upload, a time dimension called
time; the time dimension is now named by its frequency ("156 weeks", "36 months"). - web (found by the M3.5 live-stack verification): a Guided user's first run of a recipe
in a project - the workflow created from the template in the same click - failed with "The
recipe did not start: Not Found": the run was posted to
/v1/workflows//runs, with the workflow id the page was rendered with. Starting a run can now name its workflow, and the recipe runs the one it just created. - client: members could not be given the
reviewercapability (needed to approve validations) without raw HTTP:tenants.add_member(..., capabilities=[...])andtenants.set_capabilities(...). - workflows:
compare.diebold_marianowith no more paired errors than the horizon (three folds tested at horizon 4) returned a NaN p-value and failed the node with an internal validation error; it is now a plain data error that says to add folds, and a non-finite long-run variance is refused like a zero one. - Web dev server: opening the app as
http://localhost:5173now redirects tohttp://127.0.0.1:5173. Before, the API's CORS allow-list andSameSite=strictsession cookie only matched127.0.0.1, so every call failed and the home page said the API was not reachable.
Fixed (M3.5 review: modeling and statistics)¶
- spec, server: a schedule whose run was missed during the repeated hour of a
daylight-saving fall-back (or that was created or edited in that hour) fired on every
workflow-loop tick:
next_firecompared local times and returned the first occurrence of the hour, which was already in the past. It now returns only times strictly after the given instant (a repeated local time still fires once), and the schedule loop refuses anext_run_atthat is not in the future. - spec (security): a project policy could weaken a tenant policy's check parameters
(for example
data.qualitymax_missing: 1.0, or an emptyplausibility.signsexpected): parameters of a check named by both were overwritten by the project's. Parameters now merge by strictness (the stricter number, the union of lists and of expected signs, the tenant's value otherwise), so tenant policies are a floor for parameters too. - spec, server, web: Help me choose with a goal no starting point was made for ("Check or prepare my data") listed every model and template anyway, the first as "Best match" at 0% with "Not made for: check or prepare my data". An item made for other goals is now never recommended; when no model or template fits the goal, the building blocks that do (preparation steps, workflow nodes, governance checks) are ranked instead; the time-series readiness workflow template is now offered for "Check or prepare my data"; and the web app only calls an item the best match when it fits more answers than it misses.
- spec, server: the About card of
mmm.frequentistsaid it gives estimates "without the ranges a full Bayesian model provides" and "bootstrap ranges optional", although its bootstrap (at least 20 replicates) always produces ranges; it now says it gives simpler bootstrap ranges instead of full Bayesian uncertainty. The main MMM card listed the modelsmmm.nestedandmmm.multivariate, which are not installed, as related items (shown as bare ids);GET /v1/catalog/aboutsnow leaves out related items that are not in the catalog. - spec: the gallery template Series that move together (VAR or VECM) failed on
stationary series (growth rates, changes): Johansen rejects every null, the rank equals
the number of series, and the template sent that rank to
ts.vecm, which refuses it. Stationary series now get a VAR on their levels (a newstationarybranch), and the VECM's rank is kept between 1 and one less than the number of series. The cookbook example and the workflows guide show the same three paths. - spec, workflows (statistics): the canonical Time-series readiness to ARIMAX template
over-differenced seasonal series: its unit-root tests ran on the raw levels, so a seasonal
random walk (no first difference needed) got a first difference on top of the seasonal
one in 93% of simulated draws. The seasonal decision now comes first and the unit-root
tests run on the seasonally differenced series, with an over-differencing guard (no
first difference when it would leave a lag-1 autocorrelation below -0.5); in simulation
the seasonal random walk is first-differenced in 1 of 30 draws and plain random walks
are still differenced. Its AR/MA order search also scored models without the seasonal
difference the final ARIMAX uses and searched every period, including the backtest's
test windows:
search.arma_ordernow takesDandperiod(it fits ARIMA(p,d,q)(0,D,0)s, the model that is fitted) and an optionalsplitplan (it searches only before the plan's first test window), and the template uses both. - workflows (statistics):
test.grangerdecided on the smallest p-value over lags 1..maxlagwithout correction, so an unrelated driver "helped predict" the result about twice as often as the stated alpha (0.11 at 0.05 in simulation). The verdict's p-value is now Holm-corrected for the lags tried (size 0.05 in simulation;correction: holm), or the exact F test at one prespecifiedlag(new parameter); the per-lag p-values and the uncorrectedmin_p_valuestay as descriptive results. - workflows (statistics):
compare.diebold_marianotreated one h-step error per backtest fold as errors from consecutive forecast origins: its long-run variance used h - 1 autocovariances in fold units and the small-sample factor of horizon h, which for folds 4 periods apart at horizon 4 shrank the statistic by 0.41 (or left no variance at all), and refused horizon 8 on 6 folds. Foldsstepperiods apart only overlap up to ceil(h / step) - 1 folds; the node now reads the spacing from the folds (backtest and benchmark metrics carry each fold's origin,origins) or astepparameter, and uses that many autocovariances with the matching correction (details.variance_lags). - runner (statistics): the Model Explorer's comparison ran Diebold-Mariano tests with
horizon 1 on the held-out errors of time-series forecasters, which are 1..H-step errors
from one forecast origin (a single, strongly dependent path): equally accurate forecasts
were called different in 74% of simulated cases. For such models (statsmodels and the
other
tsforecasters) the comparison now shows the metrics and rankings without a significance test and says to compare backtests instead; for models that predict from each period's observed drivers the test uses a Newey-West number of autocovariances. - spec, runner, web (statistics): the Data Explorer's cross-correlations were raw by
default and judged against the white-noise band
1.96 / sqrt(n), valid only when one series is white: for two independent persistent series (AR(1), phi = 0.9) 53% of the lags were flagged significant and every run had a spurious peak. Prewhitening is now on by default (prewhiten: true, also in the web's explore dialog), and the band follows Bartlett's formula for two autocorrelated series (it is the white-noise band when either series is white), so the raw view no longer flags such lags either (under 10% in simulation). - workflows (statistics): the residual tests of a workflow (
test.ljung_box,test.jarque_bera,test.arch_lm,test.breusch_pagan,test.white,test.cusum,evaluate.residuals) on an integrated or unobserved-components model included its diffuse first residual - the level itself, since the first prediction is 0 - so for ARIMA(0,1,0) on a series around 500 Jarque-Bera gave p = 0.0 instead of 0.47. The fit's own burn-in (statsmodels'loglikelihood_burn, stored per series) is now dropped. - runner (statistics): the governance check
residuals.whitenesspassed (Ljung-Box p = 1.00) on strongly autocorrelated errors of unobserved-components and other diffuse-initialised statsmodels models: it only dropped the start-up periods of an (S)ARIMAorder, so the first residual - the level itself - stayed in and swamped every autocorrelation. The fit's own burn-in (stored per series) is now dropped as well. - statsmodels plugin, runner (statistics): the Ljung-Box test stored with every
statsmodels fit (the
RESIDUAL_AUTOCORRELATIONdiagnostic and the Model Explorer's residual view) reduced its degrees of freedom by every parameter - constant, drivers and the variance - so a correctly specified regression with AR(1) errors was flagged autocorrelated 23% of the time (and the rule silently switched to no correction when a model had many parameters, e.g. ETS). It now counts ARMA terms only (ar.L*,ma.L*, seasonal terms, ARDL/ECM lags of the target), stored per series asmodel_df, whichresiduals.whitenessalso uses (it previously ignored seasonal AR/MA terms). - runner, spec, web (statistics, critical): the governance null test
null.permutation_targethad the wrong size and no power on time series. It shuffled blocks of the result's levels (which keeps the series' own persistence and adds a level jump at every block edge, including the train/test boundary) and compared raw holdout errors, whose spread came from which levels landed in the test periods: in simulation it rejected 35% of random walks and 33% of real AR alternatives, and a marketing mix model with no media signal passed. It now compares a scale-free statistic - the held-out skill against the null forecast (the last value of an integrated series, else its mean) on each dataset - under a null that matches what is tested: a model with drivers keeps its result and gets its drivers shuffled in time (so trend, season and controls cannot pass on their own; a model that ignores its drivers is tested as one without); a model without drivers gets the d-th differences of its result shuffled and re-integrated from the same start (d from its ARIMA order or ADF/KPSS). In simulation the test keeps its 5% size on random walks and rejects real dynamics far more often; data without a time order keep the classic row permutation of the result. The evidence names the null and the statistic, and the web's null-distribution chart labels its axis from it. - runner, spec (statistics): the governance check
null.noise_regressorscould never pass for a Bayesian marketing mix model: mmm-framework's channel effects are positive, so every posterior draw of a placebo channel's effect is above 0, its 90% interval never covers 0, and the check reported "clearly not zero" and WARN or FAIL even at a 0.5% share. A Bayesian model is now flagged when a placebo's share is above the warn limit with at least 90% posterior probability (practical equivalence); the share is taken of the real drivers' total effect (of the result only when that cannot be measured), so the limit does not depend on the size of the baseline. Placebo channels also inherited the source channel's per-channel priors and calibration (a tight business-prior or lift-test ROI), so a calibrated model failed because of the prior: they now get the model's default prior (carry-over and saturation shapes are still copied). Frequentist placebo p-values are judged with Holm's correction for the number of placebos (the uncorrected 5% rule flagged a good model 14% of the time with three placebos). - runner, mmm plugin (statistics): the governance check
sensitivity.priorwas always skipped for a defaultmmm.bayesianmodel ("the model's priors are not set in its settings"), although the default MMM policy lists it: mmm-framework's default priors are not in the spec's settings. The check now writes out every prior the model uses before scaling - the plugin's settings defaults (seasonality.prior_sigma,trend.growth_prior_sigma,hierarchy.media_geo_sigma), the defaults a plugin declares for values its engine fills in (mmm.bayesian:priors.roi_default, LogNormal(0, 1)) and business priors translated at fit time - and no longer multiplies Beta/Gammabetaorbparameters, which moved the prior instead of widening it. - workflows (statistics): nested split plans were not nested.
search.gridignored the inner folds of asplit.nestedplan and picked its winner by the outer folds' score - the optimistic selection bias nested cross-validation exists to remove - and reported that score asbest_score; with a k-fold outer plan,search.gridandmodel.backtestsent the plan to the time-window scorer and failed ("horizon.periods ... -90"). Each outer fold now tunes on its own inner folds, refits the inner winner on its outer training window and scores it on the outer test window:best_scoreis that honest estimate (new resultsnested,outer_scores,selected_per_fold), and the outer plan's kind decides how folds are scored. - mmm plugin (statistics): a lift test that cut a channel's spend by up to 25% was
mapped to mmm-framework's marginal-ROAS estimand for a spend increase of the same
size: the experiment measured the slope below today's spend, the model was fitted to
the slope above it, and under diminishing returns the backward slope is larger (1.49
times for
1 - exp(-2x)at a 20% cut), so the calibration pushed the saturation curve (and the budget optimiser) away from what the experiment measured. mmm-framework'sMROAStakes only increases and its off-panel route has no marginal estimand, so partial cuts are now skipped with the reason (listed inlearned.calibration.lift_tests.skipped); increases and full holdouts calibrate as before. - web, runner: the Data Explorer overview's missingness chart printed duplicate rounded ticks ("0% 0% 0% 0.1% 0.1% ...") on small ranges - its axis had a fixed precision; Vega now picks the precision from the tick step (the tooltip keeps two decimals). The overview and the quality section also said "No data problems found." above a list of good-to-know items (a little missing spend, extreme values); they now say "No problems to fix; 2 things to know."
- runner, spec (conformance): MFF ingest disagreed with mmm-framework's MFF semantics,
although the two are documented to give the same model. National rows (a blank
Geography) became a phantom''geo - national TV only there, sales missing there - where mmm-framework'sMFFLoaderspreads a national variable across the geos; and rows that differ only byCampaign,OutletorCreativebecame their own dims where mmm-framework sums them. Themffpreset now follows mmm-framework: a blank (orNational,Total,All) label is the coarser level, and a variable given only there is spread across the levels (geographies equally, products by the KPI's share, as mmm-framework's defaults; newMffOptions.allocationandkpi), a variable may not mix the two levels, and the breakdown columns are summed unlessMffOptions.splitkeeps them as dims. Ingest notes say what was spread. A parity test compares the panel withmmm_framework.load_mffon national media and campaign rows.
Fixed (M3.5 review: platform server)¶
- server (security): a submitter could skip the tenant's strict policy - and the
second-person approval it requires - by naming a lenient policy (even one for another
model family) in
policy_ids, and the promotion gate only looked at the latest status. A validation is now always held to every policy in force (tenant floor and project);policy_idsonly adds policies. The gate accepts avalidatedversion only when that validation covered every gating policy in force as it stands now (by version, or by checks, limits, settings and reviewers at least as strict); a version validated before a policy was added or made stricter reads "validated under other or older policies" and may be validated again. - server: two simultaneous validation requests for one model version (or a request and
a workflow's
submit: true) both passed the "no open validation" check and created two validations and two governance runs. The version row is now locked while the check runs. - server (security): a workflow's
governance.policynode withsubmit: truerecorded its validation under the one policy the node named, dropping the tenant floor, and as submitted bysystem, so the person who started the workflow could approve their own model. The submission is now held to every policy in force (evidence missing a required check fails), is submitted by the run's creator (the schedule's or trigger's creator for automated runs) and the second-person rule applies to them; a validation no person submitted cannot be approved (reject and resubmit it). - server, workflows: when a node failed while starting (invalid settings, a missing
input), the run's fail-fast did not stop the siblings already started: the list of nodes
to stop lived in memory only and was dropped before the next pass, so a long fit ran to
the end before the run reported
failed. Running siblings are now stopped in the same pass, and a restored failed or cancelled run lists them again. - server: cancelling (or failing) a workflow run while a
search.*node was between its backtests and the winner's refit started the refit anyway - afitrun nothing cancelled, billed and registered after the run was cancelled. The refit is no longer started once the run is stopping, and a child started in the same pass is stopped by its current run id. - server:
search.grid(and the other searches with a winner refit) always failed withSPLIT_INVALID"model.fit takes a holdout plan" when its split was a backtest plan - rolling origin, expanding window - which is what a search needs: the refit of the winner reused the backtest plan as a fit split. The winner is now fitted on all the data for a backtest plan (a holdout plan is still kept). - spec, server (security): an exception other than
MapiErrorwhile advancing a workflow run - a 400-digit number parameter multiplied by a float in a$expr(OverflowError), or deeply nested data compared in a condition (RecursionError) - escaped the orchestrator's pass, left the runqueueduntil its deadline (up to 7 days) and, with enough such runs at the head of the loop's queue, stopped every tenant's workflows. The condition language now raises onlyEXPRESSION_ERROR(arithmetic guarded, equality iterative and bounded to 64 levels), number parameters (and defaults) are limited to 1e15 in magnitude, and a run the orchestrator cannot advance is failed (INTERNAL_ERROR) with its children stopped instead of being retried at the head of the queue. - server (security): sub-workflow runs were exempt from
MAPI_WORKFLOW_MAX_ACTIVE_RUNS, so one start of a workflow with 32subworkflow.runnodes (and nesting up to 8 deep) created far more active runs than the limit. Sub-workflow runs now count against it, and asubworkflow.runnode over the limit fails withQUOTA_EXCEEDED. (The children's executions were and are held by the dispatcher's per-tenant concurrency and vCPU limits.) - server (security): schedules and dataset triggers kept firing - and spending quota -
for a member who had been removed from the project, demoted, or whose API token had been
revoked: nothing re-checked the creator at fire time. They now pause themselves
(
active: false, audited asworkflow.schedule_update/workflow.update) when the creator may no longer run workflows there, and their runs are created by the person (a token's owner, with the token recorded) rather than by the token id. - server: a dataset version that became ready while its trigger was firing could be lost
for good: the finalizer wrote the new
pending_versionover the fired row without itspending_since, so no pass ever selected the trigger again until another version arrived. The pending version and its time are now set together in one atomic update that waits for a firing pass holding the trigger. - server: a workflow run's
costleft out everything its sub-workflows ran (asubworkflow.runnode's child is a workflow run with no attempts of its own); the roll-up now follows sub-workflow runs down to their node runs. - server:
POST /v1/workflow-runs/{id}:cancelracing the run's completion reopened a finished run ascancelling; the next pass flipped it back and sentworkflow.run.succeeded(event and webhook) a second time. The cancel now locks the run and leaves a finished run as it is. - server:
POST /v1/runs/{id}:retryon a failed workflow run crashed with a500(NotImplementedError), and on a failed workflow node or governance run created a run that nothing collected. These are now refused with409 CONFLICTnaming what to do instead: start the workflow again (unchanged nodes come from the cache) or resubmit the validation. - server: workflow node runs carried no plugin label, so the Docker and Cloud Run
executors ran every node in the default
base-bayesimage, which does not ship the statsmodels plugin: backtests, forecasts, evaluations and governance checks ofts.*models (the readiness-to-ARIMAX template) could not resolve their model in production images. A node run now goes to the image of itsmodelparameter, or of the plugin of the model version bound to it.
Fixed (M3.5 integration)¶
- web, spec: requesting a validation still offered "Policies it is held to" as
checkboxes, all ticked, as if unticking one left it out. Since the review the server holds
every validation to every policy in force (
policy_idscan only add policies), so an unticked policy applied anyway. The form now says every policy in force always applies and its checkboxes, unticked by default, add a policy; the mock and theValidationRequestdescription say the same. - docs: the governance outputs in What's new, the workflow cookbook and the null-tests guide were captured again on the integrated stack: the shuffled-driver test now passes on the cookbook's ARIMAX (p = 0.048) instead of failing it, and the note about the old null's weakness on random walks is gone.
Known gaps¶
- No human testers yet. No non-technical user has tried the Guided recipes, Help me choose or the About cards, and no reviewer has used the approval screens on real models. The M3 tester protocol applies, with the readiness recipe as an extra task.
- Real Google Cloud and Docker are unverified. Every workflow node, validation,
exploration and optimisation ran on the
local-processexecutor against the GCS shim. The images (in particularbase-bayeswith mmm-framework, JAX and nutpie, andbase-forecastwith statsmodels and the workflow runtime), thedockeranddocker-gvisorexecutors and Cloud Run were never run. Routing a workflow node to the image of its model's plugin is tested with fakes only, and a node whose models live in different images is not refused. The licence files and notices the images copy are checked statically. - mmm-framework's extensions are not exposed:
mmm.nested(nested and mediated effects) andmmm.multivariate(several outcomes, cross-product effects) are not in the catalog. Partial spend-cut lift tests are skipped with the reason, because mmm-framework's marginal ROAS estimand only takes spend increases. - Plotly static rendering still needs kaleido and a system Chrome and has not run in an
image (
MAPI_RENDER_PLOTLY_ENABLED); Vega-Lite renders out of the box. - The null tests' size and power were simulated on statsmodels, regression and a small PyMC
channel model, not on
mmm.bayesianormmm.frequentist(minutes per refit); they share the same code paths. The noise-regressor share is now measured against the real drivers' effect, so existing 2%/5% limits are stricter for models with a small media share; tuned policies may want a review. - The Data Explorer's prewhitened cross-correlation (the new default) hides a driver whose spend follows a fixed periodic flighting pattern; the raw view is one switch away.
- Child runs a workflow's orchestrator starts are bounded by the active-run limit and the dispatcher's per-tenant concurrency and vCPU budgets, but not by the per-token admission quotas, which need a request. Schedules and triggers re-check their creator only when they fire (they pause themselves then); the reason is in the audit log, not in their API representation.
mapi workflows run --watchand the engine's skip reasons still say "took the then arm"; the web monitor and reports use the plain explanations.- Help me choose ranks
ts.hsgp_trendfirst for a beginner forecasting one series with drivers, although it does not use them (ts.sarimaxcomes first for other experience levels). - The licences of a few Python packages with ambiguous metadata come from a hand-kept table in
scripts/third_party_notices.py; re-check it when the lock bumps one of them. - The documentation's captured terminal outputs are kept in step by hand, and the Model Explorer comparison screenshot still shows a Diebold-Mariano verdict for forecasters.
- Timing-sensitive tests can fail once on a fully loaded laptop (the canvas drag test of the workflow editor, the runner's checkpointed-chain test) and pass when run again.
[0.4.0] — M3: Guided mode and integrations — 2026-10-16¶
Analysts who do not write code can now build a model from a spreadsheet, read it and try
scenarios on it, and teams can wire a model they trust into the tools they already use.
Everything was verified on a laptop against a live local stack (the GCS shim, embedded
Postgres, the local-process executor). The real Google Cloud, Docker and the five
non-technical testers are still to come (see Known gaps). In summary:
- Guided mode: a task flow for non-technical analysts. Pick a template (geo marketing mix, weekly forecast, demand by store, customer choice), upload a CSV, Parquet, MFF or Meridian table, confirm the detected column roles, state business beliefs (ROI or contribution-share ranges, a baseline) and lift tests, fit, and read a plain-language report with a scorecard. What-if sliders answer at once from a per-draw response grid (a TypeScript port of the model's own maths), and "Run exact" checks a scenario, a forecast or a counterfactual with a real score run.
- modelling: business priors translated into model priors (
translate_priorsruns and a preview chart) and lift tests as calibration inmmm.hierarchical_geoandmmm.pymc_marketing; themmm.bayesinsightbuilt-in and a BayesInsight importer; a scorecard per version and a promotion gate that needs an audited admin override for a failing or unevaluated version. - endpoints and integrations: endpoints with aliases, promotion, history and rollback; generated snippets (Python, TypeScript, curl, R, Apps Script, notebook, Atlas); score answers with the summary inline; webhooks signed with HMAC, behind an SSRF guard, with an outbox, a delivery log and replays; saved plots, static PNG/SVG/PDF rendering with vl-convert and expiring embed links served by the plot sandbox; the Integrations hub in the web app.
- trust and data lifecycle: self-service sign-up with e-mail verification, identity verification and Sybil flags with a review queue, trusted-proxy client addresses, per-tier draw and egress caps, GDPR export and tenant deletion with a grace period, and a Cloud Armor policy in Terraform.
- client and CLI: the v0.4 Python client (endpoints, webhooks, plots, Guided, tenants)
and Atlas
RemoteModel, which lets Atlas optimise a budget against an endpoint;mapi endpoints,webhooks,plots,guided,priors,signupandtenant. - docs: a new landing page, Why modeling-api, use cases, a Tutorials section (Google Sheets, embeds, BayesInsight, webhook receivers, a first Guided analysis, Atlas), Guided and Integrations guides, the security model for v0.4, and the M3 verification report.
Every workspace package and both web apps are versioned 0.4.0. Contract v0.1 gains only
additive changes. Upgrading from 0.3 runs migrations 0007 to 0010 (Guided drafts,
endpoints and webhooks, saved plots and embeds, the trust lifecycle). Multi-tenant
deployments must now set MAPI_TRUSTED_PROXIES (none when clients connect directly), or
the server refuses to start.
Added¶
- web: the Integrations hub (
/integrations). Guided cards for Google Sheets (the endpoint's Apps Script and ascore:<alias>token), a BI embed link and a webhook; endpoints and aliases with the promotion dialog (the version's scorecard, the expected gate, the server's reasons on409and the audited admin override), history and rollback, the frozen input contract with Try it (validate or score the example call), snippets for Python, TypeScript, curl, R, Apps Script and Atlas plus an Open-in-notebook cell; webhooks with an event picker, the secret shown once, rotation, Resend newest (a replay of the newest delivered event; there is no synthetic test event), the delivery log (status, attempts, response code) with replays and the auto-disabled notice; saved plots with PNG/SVG/PDF downloads (Plotly disabled with the server's reason onMODE_NOT_SUPPORTED) and expiring embed links with allowed sites and revocation; a Share button in the plot gallery;/tokens?preset=score:<alias>. - web: the trust pages. Public
/signup(disposable-address and terms errors at their fields, the "check your inbox" state, the development outbox link) and/verify-email; Settings > Data for organisation admins (GDPR export and download, tenant deletion with the typed confirmation, grace period, cancel and per-store progress); a banner for scheduled deletions; in the admin console, the sign-up review queue and per-tenant identity verification and deletion status with a retry. - docs: The Integrations hub guide (with screenshots) and the Integrations section of the
web app guide;
make docs-screenshots-integrations. - spec: the M3 contracts for Guided mode and integrations, shared by the server, runner, web app and client components (contract v0.1, additive only).
mapi_spec.guided: four guided templates (geo_mmm,weekly_forecast,store_demand,customer_choice- the last needs a built-in choice model that is not shipped yet), plain-language questions, role detection from column profiles, drafts (ingest mapping +ModelSpec+ YAML) and the BayesInsightmodel_def.jsonimport.mapi_spec.ingest: ingest presets (mff,meridian,bayesinsight), wide variables and themffformat;IngestMappingmoves here (still exported bymapi_spec.api).mapi_spec.calibration: business priors (ROI or share ranges with a credibility), lift tests, theModelSpec.calibrationblock and the sandboxedtranslate_priorsrun kind.mapi_spec.scorecard: the check catalog, items with check ids, categories and docs anchors, and the promotion gate (pass → allowed, warn → admin override with a reason, fail → blocked);Scorecardmoves here (still exported bymapi_spec.api).mapi_spec.whatifandResponseGridView: per-draw response grids, the slider interpolation contract, "Run exact" and forecast/counterfactual presets;ScoreRequest.future_covariates.mapi_spec.endpoints,mapi_spec.xrjson,mapi_spec.snippets: endpoints and aliases, score calls with inline XrJSON, input-schema compatibility, example payloads and pure snippet generators (Python, TypeScript, curl, R, Apps Script, Atlas).mapi_spec.webhooks: events, envelope,Mapi-Signaturesigning and verification, retries and the SSRF policy;mapi_spec.sharing: saved plots, renders (PNG, SVG, PDF) and embed links for platform recipes;mapi_spec.trust: sign-up, e-mail and identity verification, tenant deletion and GDPR export.mapi_spec.api.M3_ENDPOINTS(45 operations) with their access rules; new permissions (webhook,embed,tenant_deletion,tenant_export, plot delete, endpointmanagefor gate overrides), scopes (endpoints:promote,webhooks:manage), audit actions, problem codes andProject.guided_locked.- docs: Guided mode and Integrations concept pages; the v0.4 section of the HTTP API reference.
- server: the Guided data path.
GET /v1/guided/templates(paged) and/{template_id};POST /v1/guided/detect-rolesfrom browser profiles or a stored dataset version;POST /v1/guided/draftsandPOST /v1/guided/import/bayesinsight, validated statically against the catalog with the schema the draft's mapping promises. Every operation documents its RFC 7807 problems in the OpenAPI document. - Migration
0007_guided:projects.guided_lockedanddataset_versions.columns. - The persona lock is persisted, audited (
project.guided_lock) and enforced: in a locked project, members below admin store Guided specs only, and only theirbasicfields may change (403 FORBIDDENwith the pointers otherwise). - The finalizer validates and stores an ingest's
columns.json, which detection reads and…/artifacts/columns.jsonserves. - runner: sandboxed ingest for Guided mode: wide variables (one column per channel or
product), the Meridian preset, MFF tables mapped like a CSV after the pivot,
MffOptions.combine, and the BayesInsight preset (allowed_*values, period range, necessary variables). Caps on rows, columns, cell and name length, and decompression (gzip CSV streamed under a budget, Parquet, zip and netCDF by their declared sizes) are checked before a parser allocates; every refusal is a plain message with a suggested action. Ingest writescolumns.json, the profile of every uploaded column. - plugins:
choice.multinomial_logitinmapi-plugin-tabular, a conditional logit with household product effects and Bayesian-bootstrap draws, so thecustomer_choicetemplate is real. Thee84_liteconformance fixture replaces the corpus deferral. - spec:
mapi_spec.bayesinsight.import_bayesinsightmapsmodel_def.jsonontommm.hierarchical_geo- adstock, Hill saturation and pooling with BayesInsight's own priors where they have equivalents, controls, seasonality, trends - and reports every feature without one as anIMPORT_UNSUPPORTEDwarning (BayesInsightImport.targetkeeps themmm.bayesinsightdraft). Role detection gainsColumnProfile.monotonicandorigin,profiles_from_dataset,draft_schemaandguided_lock_issues;GuidedAnswers.formatacceptsmff;IngestJobgains the ingest caps. -
docs: the "From a spreadsheet to a Guided draft" guide; the Guided mode concept page, the built-in models and the conformance guide describe the served parts.
-
mmm:
mmm.hierarchical_geodeclaresbusiness_priorsandlift_testscalibration. - Business priors (ROI or contribution-share ranges at a credibility, a baseline share)
become per-channel
LogNormalbeta_channelpriors (and aninterceptprior) through a vectorised location/scale bisection over Monte Carlo draws of the other priors, with a prior-predictive preview in business units and the prior mass inside each stated range (mapi_plugin_mmm.calibration; property tests check the round trip). - Lift tests add
kpi_delta ~ Normal(implied lift, se)over the test window; the extra spend is carried over through the lag tensor and the implied lift is kept aslift_tests__model. response_curvescarries the baseline (intercepts and controls), closing the expected KPI.mmm.pymc_marketingtakes lift tests through pymc-marketing'sadd_lift_test_measurements.- runner:
translate_priorsruns (priors.jsonand themmm.prior_beliefs/core.prior_predictiveplots); every fit writesscorecard.json(the whole check catalog, or the guided template's, each measured or skipped with a reason) and the what-if grid viewui/grids/response_curves.json; forecast and counterfactual score runs writeui/guided/forecast.jsonandui/guided/counterfactual.json, and a forecast's fan chart shows the fitted history with the split line. Lift tests are checked against the raw data before a fit. - server:
POST /v1/guided/prior-translationsandGET …/{run_id}(static checks of the beliefs and lift tests against the dataset summary, a pinnedtranslate_priorsrun, its verifiedpriors.json). The finalizer stores the runner's scorecard on the model version and validatespriors.json; spec validation checks a spec's lift tests against the dataset. - sdk:
FitContext.calibrationandFitContext.to_original(the fitted scales);prepare_model_stagefor runs that build but do not fit a model; the optionaltranslate_priorsplugin hook. sdk-pymc:PyMCModel.prior_overlap_varskeeps cheap prior draws for the scorecard's newplausibility.prior_overlapcheck. - sdk/runner/server: forecasts extend the covariates their payload lacks
(
ScoreRequest.future_covariates:carry_last,seasonal_naive,zero) from the fitted history (mapi_sdk.future); the server passes the fit dataset as the score run'shistoryinput.mapi_server.services.scorecards.promotion_decisionderives the promotion gate from a stored model version. - plots:
mmm.prior_beliefs(each channel's prior ROI or share against the stated range). - docs: Guided modelling (the translation math, the lift-test likelihood, how each scorecard check is measured, the what-if, forecast and counterfactual views).
- server: endpoints and aliases (
/v1/endpoints, migration0008_endpoints_webhooks). PUT /v1/endpoints/{alias}promotes a model version through the promotion gate: the sanity scorecard, the endpoint's evaluation policy (evaluate_then_refitby default, so endpoints serve versions refitted on all data after their holdout evaluation) and input-schema breaks. Awarnneeds a signed-in project admin's override with a reason (auditedendpoint.promote_override; no API token can override); afailblocks.expected_revisionguards concurrent promotions.- History, rollback (audited with its reason) and delete;
GET …/schemawith an example payload,POST …/validate, andGET …/snippetsrendered with the server's base URL. POST /v1/endpoints/{alias}/score: inline XrJSON payloads are checked against the endpoint's input contract and the CoordPlan rules, stored as run inputs and decoded only in the sandbox;Prefer: wait,Idempotency-Key(repeat the call to collect a result),score:<alias>tokens, and the per-token rate limit and vCPU budget. Bodies are capped (413) and depth-checked before parsing.- server: webhooks (
/v1/webhooks). - CRUD with the signing secret shown once and stored sealed (AES-GCM under a key derived from the server secret); rotation keeps the old secret signing for 24 hours.
- A transactional outbox:
run.succeeded,run.failed,model.version.ready,score.completedandendpoint.promotedare written in the transaction of the change;emit_tenant_eventservestenant.deletion.completed. - Deliveries from the worker on a
webhooksprocrastinate queue: signed (Mapi-Signature), retried with exponential backoff and jitter, auto-disabled afterMAPI_WEBHOOK_AUTO_DISABLE_AFTERconsecutive failed events or a410(audited), with a delivery log and replays. - The SSRF guard: static URL checks, one resolution with every address checked, a pinned
connection with the original name as SNI and
Host, no redirects, time and size caps;MAPI_WEBHOOK_ALLOW_HTTPandMAPI_WEBHOOK_DEV_ALLOWLISTfor development only. - Metrics for promotions, endpoint calls, events, delivery tries, SSRF refusals and disabled webhooks; every new operation documents its RFC 7807 problems in OpenAPI.
- docs: the Endpoints and webhooks guide and the webhook settings.
- server: saved plots, static rendering and embed links (plan §10.3 items 4-5).
POST/GET/DELETE /v1/plots[/{plot_id}]: a model version's plot (after its signed manifest verifies) or a user-authored PlotSpec, sanitized on save and on every read. User-authored plots are recorded withauthorprovenance whatever they claim.POST /v1/plots:render(Prefer: wait,Idempotency-Key) andGET /v1/plots/renders/{run_id}: arenderrun of one sanitized plot. The API writes itsplots.jsoninput and pins each bundle dataset as aplotdata.<n>input. Plotly is refused withMODE_NOT_SUPPORTEDunlessMAPI_RENDER_PLOTLY_ENABLED.POST/GET/DELETE /v1/plots/{plot_id}/embeds[/{embed_id}]: tokenised links for platform recipes only (EMBED_NOT_ALLOWEDotherwise), stored as keyed hashes, with expiry, revocation, view counts and audit events. A PNG fallback is rendered when the caller may start render runs, and copied write-once into the bundles bucket on first use.- The public
GET /v1/embeds/{token}: no credential is read (cookies andAuthorizationare dropped), CORS for the plot-sandbox origin only, rate limits per link and per address, every dataset inlined from the verified bundle without decoding it. - Migration
0009_plots_embeds:saved_plotsandplot_embedswith forced row-level security; the gateway role may read only the embed lookup columns. - Every new operation documents its RFC 7807 problems in the OpenAPI document.
- runner:
renderruns draw PNG, SVG and PDF with vl-convert (pinned in the newmapi-plots[render]extra) with network access off, refuse anything that could fetch a resource, readplotdata.<n>inputs, applywidth/heightand cap images at 32 MiB. Plotly needs kaleido and a system Chrome (MODE_NOT_SUPPORTEDotherwise; no browser is ever downloaded). - plot-sandbox: the embed page
/embed/<token>. It fetches the embed view with no credentials under its own CSP (connect-srcadds only the API's/v1/embeds/path), validates and decodes it, honours the link's allowed origins and offers the PNG fallback. Themapi/webimage serves it with the API origin filled in at start-up. - docs: the "Sharing plots" guide and the new settings in the configuration reference.
- spec: the sign-up review queue (
SignupReview,SignupReviewDecision, theSYBIL_FLAGScatalog),GET /v1/admin/signups,POST /v1/admin/signups/{signup_id}/reviewandPOST /v1/admin/tenants/{tenant_id}/deletion/retry; theauth.signup_flagged,auth.signup_reviewandtenant.deletion_retryaudit actions; the deletion record and platform logs inRETAINED_RECORDS. - server: the trust lifecycle (migration
0010_trust_lifecycle). - Self-service sign-up (
POST /auth/signup,POST /auth/verify-email): single-use e-mail tokens stored as HMACs (24 h), the same answer for new and known addresses, a mailer protocol (SMTP with required TLS, a file outbox read atGET /dev/outboxin thedevprofile, console), the disposable-domain blocklist plus configured domains and files, per-address, per-domain and platform-wide sign-up limits, and Sybil heuristics (same address, browser cookie, e-mail, domain burst) that flag sign-ups for the operators' review queue. Verification creates apublictenant, itsdefaultproject and the admin grant. - Identity verification (
GET /v1/tenants/{id}/verification, operators'PUT /v1/admin/tenants/{id}/verification, anIdentityVerifierprovider protocol with a fake) and cut line A at run admission: uploaded code needs a trusted tier or identity verification (IDENTITY_VERIFICATION_REQUIRED); public tenants run built-ins and declarative specs only. - Tenant deletion: a confirmed request suspends the tenant and cancels its runs; after the
grace period the orchestrator makes it
deleting(tokens revoked, sessions ended, revoker hooks) and a procrastinate job purges the queue, every tenant table, everyt/{tenant}/object prefix (resuming after retention locks), then notifies and writes thetenant.purgedtombstone; progress per store; automatic and operator retries. - GDPR export: a zip of every tenant record (credentials redacted), dataset objects and bundle manifests, with a short-lived signed link.
- infra: a Cloud Armor policy on every load-balancer backend (OWASP CRS sqli, xss, lfi,
rce and scanner rules in preview-then-enforce, per-IP rate bans on sign-up,
/auth/*, embeds and endpoint scoring, Adaptive Protection), and an explicit retention for the control project's_Defaultlog bucket (tenant deletion does not purge logs). - docs: Sign-up and abuse controls, Deleting and exporting data, and a Cloud Armor section in Security hardening.
- integration: the M3 parts work together.
- Tenant deletion revokes the tenant's embed links and disables its webhooks when it starts,
purges every M3 table (endpoints and their history, webhooks, events and deliveries,
saved plots, embeds, column profiles) and then delivers a signed
tenant.deletion.completedto the webhooks that subscribed to it, through the same SSRF-guarded sender as every delivery (mapi_server.services.tenant_hooks). - Webhook events come from the finalizer's terminal transitions (
run.*,model.version.readywith the runner's scorecard), promotions and rollbacks (endpoint.promoted), score runs (score.completed) and deletions. - Endpoint promotion gates on the scorecard the runner measured at fit
(
services.scorecards.promotion_decision, the single gate derivation). - Sign-up workspaces are
publictier: built-ins and declarative specs only, on executors with enforced egress and DNS (cut line A). - server:
POST /v1/guided/requests:compileturns a what-if scenario, a forecast or an intervention preset into itsScoreRequest, checked against the model version's score metadata ("Run exact"). - runner: score runs read an endpoint call's inline XrJSON payloads (
inline.data,inline.scenario) with the wire-format caps and the model's dim types. - mmm:
mmm.bayesinsight, a BayesInsightmodel_def.jsonas a built-in model (Q8): Normal, log-normal, Poisson, negative binomial and zero-inflated likelihoods; coefficient and intercept priors in the KPI's own units; Hill, linear and S-shaped curves before delayed, geometric, Weibull or no carry-over; enforced sign constraints; fixed and partially pooled effects per row id (new levels pooled); Fourier seasons and local spline trends. Contributions sum to the expected KPI (a log-mean Divisia split for log links); lift tests observe the incremental KPI under the model's own likelihood. It passes the contract kit;mapi_plugin_mmm.bayesinsight_examplessimulates a project with known parameters. - spec:
mapi_spec.bayesinsight_modeltypesmodel_def.jsonas the plugin runs it (themmm.bayesinsightsettings);import_bayesinsight(target="mmm.bayesinsight")now drafts a runnable spec - every row id a panel dim, the KPI bound toy, lagged media - falling back to BayesInsight's defaults withIMPORT_UNSUPPORTEDwarnings (andinfoissues for faithful but surprising behaviour). - mmm: business-prior translation for
mmm.pymc_marketing(1.1.0): ROI and share ranges become per-channel log-normalsaturation_betapriors and a baseline share aninterceptprior (narrowing the control and seasonality priors when needed), solved on prior draws of pymc-marketing's own model;settings.priorshands them to pymc-marketing'smodel_config; atranslate_priorshook previews them and a fit without a translation run translates them itself. - sdk: categorical holdout metrics -
accuracy,accuracy_naive,accuracy_lift,log_scoreandbrier- for a text target scored as one probability per category. - spec/runner: the
holdout.hit_ratescorecard check (the holdout hit rate against always guessing the most popular product, in plain words) for thecustomer_choicetemplate. - conformance: the
guided_mmmfixture (thegeo_mmmGuided draft with business priors and a lift test, true to its simulation);e84_liteevaluates the categorical metrics; the dedicated BayesInsight import ingests, validates and fits end to end. - docs: the built-in models reference covers
mmm.bayesinsightand every plugin's calibration; Guided modelling covers business priors for the pymc-marketing adapter and the BayesInsight lift likelihood. - docs: the v0.4 site. A new landing page (both personas, how it works, the Guided quick
path, the v0.4 feature grid and the numbers measured in the M3 verification), "Why
modeling-api?" (compared with notebooks and ad-hoc apps, MMM vendors and libraries, and
generic model serving, with when to use something else) and "Use cases"; a Tutorials
section with new tutorials "Connect Google Sheets" (an Apps Script that writes an
endpoint's forecast into a sheet, run in the tests against stand-ins for Apps Script),
"Embed a chart in Notion, Slack or BI" and "Import a BayesInsight model" (import, check and
a local fit that recovers the simulated parameters), and "Receive webhooks" as a
tutorial with a tested Flask receiver; the roadmap with v0.4 released and v0.5 (expert
plugins, MCP, optimise jobs) next; the security model and architecture pages for v0.4;
make docs-screenshots-tutorials. - cli:
mapi tokens create --scopelists the endpoint,endpoints:promoteandwebhooks:managescopes. - dev:
make outbox(dev.py outbox) prints the dev mailer's newest e-mails and verification links;make devsetsMAPI_OUTBOX_DIRand the plot sandbox'sVITE_API_URL; compose keeps the outbox on a volume.make api-typesandmake snapshotsregenerate the web API types and the snapshots. - web: Guided mode, the task flow for non-technical analysts (plan §11 Guided 1-7), replacing the placeholder page.
- "What do you want to learn?": the templates as cards, and the analyses in progress. A
session is a stored spec (
metadata.labels.guided); every step has its own URL (/guided/<spec>/<step>) and is rebuilt from server state, so it can be resumed or shared. - Your data: the upload (CSV, Parquet, MFF; Google Meridian tables recognised) is profiled in the browser and only the column profiles go to role detection; questions with suggested answers, confidence and reasons; a live preview with a sparkline per chosen series; plain validation messages; then the dataset, the signed upload, the sandboxed ingest with the draft's mapping and the spec.
- What you believe: ROI or contribution-share ranges per channel with a confidence, the
baseline share and lift tests, a
translate_priorspreview (prior ROI/share distributions and a KPI fan in the plot sandbox) and the translated priors applied when they match the beliefs saved. - Fit the model: plain-language phases, progress, time left and cancel, live over SSE.
- Results: headline numbers, ROI and share per channel with 90% ranges, the fit and response-curve plots, and the sanity scorecard grouped by question with what to do.
- What if…: per-channel spend sliders answered instantly from the per-draw response grid
(a TypeScript port of
mapi_spec.whatif.evaluate_scenario, checked against golden cases the spec tests export), "Run exact" next to the instant answer, scenario presets (channel off, budget × k) and a forecast fan chart with its split line. - Share: promotion to an endpoint with the gate explained (a warning needs a project admin's reason), an embed link for a platform chart, and the way to the Integrations hub.
- "View as YAML" on every step (read-only Monaco) and "Edit in Expert" with "Back to Guided mode"; settings Expert mode changed are kept and marked "advanced settings applied". The Guided/Expert choice is remembered per user, and in a project locked to Guided mode members below admin see Guided screens only.
- Mock-mode handlers for every Guided call (
src/mocks/guided.ts), Vitest unit and route tests and the Playwright smoke spece2e/guided.spec.ts(the whole flow, the lock, the persona and a 375 px phone). - spec:
tests/snapshots/whatif_golden.json, golden what-if cases shared with the web app (make snapshotsregenerates it). - docs: "Your first analysis in Guided mode", a step-by-step tutorial with screenshots
(
make docs-screenshots-guidedrecaptures them from mock mode). - client: the v0.4 Python client. Every operation of
ENDPOINTShas a request builder (the coverage test now checks the whole v0.4 table), sync and async. api.guided(templates, role detection, drafts, the BayesInsight import, "Run exact" compilation,preview_priors),api.endpoints(promote, history, rollback, schema, validate, keyedPrefer: waitscoring, snippets),api.webhooks(secrets returned once; deliveries, replay, rotation), saved plots, renders (waited for, CRC32C-checked download) and embeds onapi.plots,api.signup/api.verify_email(sent without credentials), verification, deletion and the GDPR export (size-checked atomic download) onapi.tenants, and the sign-up review queue onapi.admin.Client().endpoint(alias)(RemoteEndpoint):schema(),validate(ds)andscore(ds) -> xr.Datasetwith inline XrJSON (mapi_client.xrjson, the SDK wire format); the generated Python snippet runs as-is.mapi_client.webhooks.verify(headers, body, secret)for receivers.mapi_client.atlas.RemoteModel: Atlas's model interface, structurally (Atlas is never imported at import time): KPI at the mean or as draws, batching along an independent dim into one score request, an LRU cache, backoff onRATE_LIMITED;register_with_atlas(); theatlasextra;make test-atlasruns the tests with Atlas installed in a throwaway overlay.- cli:
mapi endpoints ls|get|history|promote|rollback|schema|score|snippets,mapi webhooks ls|get|create|update|rotate|deliveries|replay|rm,mapi plots render|embed|embeds|revoke,mapi guided templates|detect|draft(CSV files are profiled locally),mapi priors preview,mapi tenant status|export|delete,mapi signupandmapi verify-email. Promotions, rollbacks, rotations, deletions and revocations ask first; webhook secrets and embed links are printed once, alone on stdout. -
docs: the v0.4 sections of the Python client and command-line guides, "Optimise a budget with Atlas" and "Receiving webhooks" (FastAPI and Flask), with offline, tested snippets.
-
integration (web): Guided mode and the Integrations hub work as one.
- The Share step links into the hub: the promoted endpoint's snippets, a
score:<alias>token, the chart link under Plots and links, and the hub itself (typed routes). A project locked to Guided mode keeps Integrations in the navigation. - Both use the same endpoint and saved-plot queries and the same promotion-gate preview (the
port of
promotion_gate), so a promotion or link made in one shows in the other at once; in mock mode they share one endpoint, history, plot and embed state. - Every snippet the app shows and copies is the server's (
mapi_spec.snippets), the notebook cell included; the mock's fixture endpoint, example call and snippets are generated frommapi_spec(apps/web/scripts/gen_integration_fixtures.py, checked bymake web-checkand CI, rewritten bymake snapshots), and a test checks each tab against them. -
spec: a
notebooksnippet language: a Jupyter or marimo cell that scores the endpoint withClient().endpoint(alias).score(...)and shows anxarray.Dataset; the client's tests run it as generated. -
e2e: the M3 exit criteria through the running stack.
tests/e2e/test_m3_guided_stack.py(inmake e2e) follows one analyst from sign-up and e-mail verification through thegeo_mmmGuided flow (CSV and MFF), business priors and a lift test, the scorecard and its gate, what-if vs "Run exact", forecast and channel-off, an endpoint scored with ascore:<alias>token, signed webhooks to a local HTTPS receiver with a replay, an embed link and its revocation, Atlas optimising a budget throughRemoteModelagainst the live endpoint, the GDPR export and a tenant purge checked in Postgres and the GCS shim.apps/web/e2e/m3-guided.e2e.spec.tsis the timed "unaided tester" script (upload → report → scenario) and the Integrations hub flow.make e2eoverlays Atlas fromATLAS_DIRwhen it is there. - dev: the
devprofile trusts a local webhook receiver: a throwaway CA and certificate for127.0.0.1/localhostin$MAPI_DATA_DIR/dev/webhook-tls/, withMAPI_WEBHOOK_CA_FILEandMAPI_WEBHOOK_DEV_ALLOWLISTset unless given. - gcs-shim:
--bundle-retention-days(default$MAPI_BUNDLE_RETENTION_DAYS) chooses the bundles bucket's locked retention when a root is created (0: none), so a test stack's tenant deletions can finish. - web: "Fit again, more carefully" on the Guided report when a convergence check warns or
fails (twice the tuning and draws and a higher
target_accept, up to a ceiling); a blocked Share step links to it. - spec:
mapi_spec.guided.freq_plain: role detection explains a detected date frequency in words ("weekly, on Sundays"). - docs: M3 verification: what was verified of the M3 exit criteria, the timings, the defects found, and the protocol and observation sheet for the five non-technical testers.
Changed¶
- spec: the
geo_mmmtemplate (1.1.0) fits with 1000 tuning steps and draws andtarget_accept0.95: the 500-draw fit failed the scorecard's convergence checks on two years of weekly data, which Guided users cannot act on, so the analysis could never go live. - web: in What you believe, channels start without a belief (unticked); ticking one starts from the sensible range. Untouched channels no longer become ROI beliefs.
- server:
GET /v1/embeds/{token}answersCache-Control: private, no-cache(waspublic, max-age=60), so a revoked link stops rendering on the next load. - spec:
mapi_spec.api.ENDPOINTSis the v0.4 surface (M1 + M2 + M3, every row served);V03_ENDPOINTSkeeps the v0.3 table andALL_ENDPOINTSis an alias ofENDPOINTS. - server: the M3 migrations run in one chain after
0006_executor_disable:0007_guided,0008_endpoints_webhooks,0009_plots_embeds,0010_trust_lifecycle. - spec: the
customer_choicetemplate (1.1.0) targets the built-inchoice.multinomial_logit; wide column labels drop the question's keywords (price_cola→cola); a draft refuses answers that would give two columns the same dataset name. - spec: the
customer_choicetemplate (1.2.0) evaluates the categorical metrics and runsholdout.hit_rate;BayesInsightImport(target="mmm.bayesinsight")drafts the shipped plugin's settings (variablesmaps analytic names to variables; the KPI isy) instead of keepingmodel_defverbatim. - server:
POST /v1/guided/import/bayesinsightvalidatesmmm.bayesinsightdrafts against the catalog too. - conformance: the corpus runner compares fit-time and evaluate-run metrics only when it
rebuilt the holdout (a
last_per_groupsplit is scored in sample).
Fixed¶
- cli:
mapi webhooks update ID [--enable|--disable] [-e EVENT ...] [--url URL] [--description TEXT]. A webhook created with--inactive, or disabled automatically after repeated failures, could only be switched back on in the browser;--enabledoes it from the command line and resets the failure count. - docs: the Guided tutorial's report and scorecard screenshots contradicted each other (media drove 23% of results, yet "about 80%" would happen without media). The mock-mode scorecard now reads its baseline, largest-channel and best-return checks from the same response grid as the report, and the screenshots were recaptured.
- server: "Run exact" failed on every live stack:
POST /v1/guided/requests:compileleft what-ifs and presets over the fit window without data. The compiled request now names the fit dataset version, and a score request without data, scenario or horizon is refused with422 SCHEMA_INVALIDbefore a run starts. - web: the Guided report showed MAPE in percent as a fraction ("83 %" for 0.83 %); it reads the scorecard's measured value. The beliefs preview no longer prints impossible baseline shares ("between -164 % and 271 %"); Share a chart offers result charts before the sampler's diagnostics.
- plots: prediction plots name the predicted quantity as the dataset does (
revenue, noty_hat). - client:
datasets.uploadaccepts anmffmapping for a.csvMFF table. - server: an unresolvable webhook host is explained in plain words (no
gaierror). - web: the New token dialog's budget period showed "pe" instead of "per day"/"per month": native selects no longer shrink below their width in a flex row (filters, the cap window, the intervention editor and the upload mapper had the same risk).
- spec:
indexdims extend contiguously (their coordinates have ordinals), so scoring anindexdim with anextendpolicy no longer fails withNON_CONTIGUOUS_EXTENSION. - sdk: cell-level splits treat an empty text target as missing and hold text targets out
with
'', so a choice panel's chosen product can be held out. - runner: the
holdout.mapescorecard check readmapi_sdk's MAPE, which is in percent, against thresholds in fractions, so a 1.4% holdout error showed as 140% and blocked the promotion of almost every model; percent metrics are now divided by 100 first. - runner: inline endpoint payloads named like the endpoint's input contract (canonical
names such as
time) failed to score when the dataset's own name differed (week); the score run renames them to the dataset's names before typing coordinates. The snapshot's documentation says which names a payload uses. - spec: an endpoint's example call (
GET …/schema, the snippets,mapi endpoints score --example) asked quantile models for ameanthey cannot serve; it now asks for the representation's default summary.
Fixed (M3 review: Guided modeling)¶
- spec/server/web: the promotion gate let through an
evaluate_then_refitversion that was never evaluated on a holdout (a spec withoutdata.splitskips every holdout check and its scorecard can still pass). Such a version now needs an administrator override with the new gate reasonNOT_EVALUATED, as does a guided version whose template's scorecard checks did not all run (Q13). The web app's gate preview follows. - mmm: a contribution-share belief for a channel with no spend in the fit window
translated to an absurd prior (
LogNormal(mu≈692), so any new spend on the channel predicted ~1e300) with no issue. Both translators (mmm.hierarchical_geo,mmm.pymc_marketing) now refuse it with an error, as they do for an ROI belief, and refuse any solved prior that is not finite or has|mu| > 50, keeping the channel's default. - mmm/runner: a response grid whose spacing skipped multiplier 1 (
grid.max_multiplier: 3,grid.points: 20, or a maximum below 1) lost its what-if sliders, and the scorecard's ROI, share and baseline checks were read at the nearest point (1.05x or 0.5x spend). The grid now always holds 1 (inserted when the spacing skips it;max_multipliermust be at least 1), the curves are read at exactly 1, and a what-if grid the view contract refuses is reported asVIEW_INVALIDinstead ofVIEW_TOO_LARGE. - runner: a model with more than 256 geos lost its whole what-if grid (the per-geo view
failed its geo cap and the national grid was dropped with it); it now gets the national
grid (
mapi_spec.views.MAX_GRID_GEOS). - server:
POST /v1/guided/requests:compileanswered404for anendpoint://model target, andPOST /v1/specs:validatereported a score request aimed at an endpoint asREF_UNRESOLVED("endpoint aliases arrive in v0.4"). An endpoint ref now resolves to the version the endpoint serves. - runner: a dense ingest (and the MFF pivot) built every combination of the dim labels
with no cap, so a 2 MB CSV whose keys rarely repeat asked for ~80 GB and the run was
OOM-killed. The cube is now checked before it is built (at most 250 million values, and
within the class's scratch budget) and refused as
PAYLOAD_TOO_LARGEwith a hint. - spec/server/runner: static renders had no pixel cap, so
scale: 8with a 4096 x 4096 size (or a plot that sets its own huge size) asked vl-convert for a ~4 GiB raster and the run was OOM-killed. A PNG or PDF now holds at most 50 megapixels (mapi_spec.sharing.MAX_RENDER_PIXELS):POST /v1/plots:renderrefuses a bigger request with413 PAYLOAD_TOO_LARGE, and the runner checks the laid-out size (composite views included) before it rasterises. - spec/runner: the
data.historycheck counted periods (warn below 104, fail below 52) whatever the data's frequency, so 105 days of daily data passed as "enough to learn seasonality". It now measures years (history_years: periods over the periods in a year at the time dim's frequency, or the one its dates follow; warn below 2 years, fail below 1; theweekly_forecasttemplate fails below 0.5). - mmm: a lift test cut by the fit window (one that straddles the end of an
evaluate_then_refitevaluation fit's training periods, or starts before the data) put all of its spend into the periods left and compared them with the whole test's KPI change, biasing the evaluation fit and the holdout scorecard. Such a test is now skipped and logged, inmmm.hierarchical_geo,mmm.bayesinsightandmmm.pymc_marketing. - runner: dates with a UTC offset (
2024-01-07T00:00:00+01:00, common in BI exports) or a tz-aware Parquet timestamp became time zone-aware labels that the ingest then refused as "not dates" (and offsets that change with daylight saving time failed outright). They are now read as the local date and time they name, with aninfoissue saying the offsets were dropped. - sdk:
future_covariates: seasonal_naivealigned the last season by position, so a horizon that started after a gap (or a payload of later periods only) shifted the seasonal pattern, and a missing value in the last season was copied forward as NaN. It now takes the value whole seasons before each horizon date, stepping back past missing values, and carries the last value forward (with a warning) only where no season has one. - plots/runner: a Guided geo MMM's fit chart titled its panels with the spec's internal
dim name (
geo) instead of the analyst's column (market), and faceted response curves saidchannelwhatever the column was called. Recipes now title facets with the dataset's name for the dim (RecipeInput.dim_label, from the bundle's/binding), and the fit's holdout prediction tree carries the binding.
Fixed (M3 review: integrations and trust)¶
- server: every per-address control saw the proxy's address, not the caller's, so the
sign-up, verification and embed-view limits and the Sybil
same_ipcheck applied to the whole platform behind a load balancer. NewMAPI_TRUSTED_PROXIES(addresses or CIDR ranges): a request from a trusted proxy takes the right-most untrustedX-Forwarded-Forhop as the client; any other peer's header is ignored. The multi-tenant profile refuses to start without it (nonewhen clients connect directly); Terraform sets Cloud Run's and the load balancer's hops andmake devsetsnonefor its multi-tenant stack. - server: a promoter, an
endpoints:promotetoken included, could switch off the schema-change and train-only gates by sending a laxerevaluation_policywith the promotion. Relaxing the policy (schema_changetowardallow,fit_modetotrain_only) now needs a signed-in endpoint admin, like an override (403 FORBIDDENotherwise, audited as a denial), and the promotion's audit event records the policy before and after (mapi_spec.endpoints.policy_relaxations). - server: the per-domain (25 a day) and platform (500 an hour) sign-up caps counted
unverified sign-ups, so made-up addresses could lock a whole e-mail domain, or everyone,
out of sign-up. They now count verified sign-ups only (the per-address limit still counts
every request), and large free-mail domains (
MAPI_SIGNUP_SHARED_EMAIL_DOMAINS, defaultmapi_spec.trust.SHARED_EMAIL_DOMAINS) have no per-domain cap and never trip thedomain_burstSybil flag. - server: the plan's draw cap (Q2: 300, 500 on paid tiers) was enforced only by the
Python client, so any caller could ask a model or endpoint for 10,000 draws. Score
requests asking for more than the tenant tier's
MAX_SCORE_DRAWS(public300, other tiers 500) are refused withQUOTA_EXCEEDEDat/output/draws, and the score job carries the cap (ScoreJob.max_draws: score runs now store a{request, max_draws}payload) so the runner enforces it too. - server: a
score:<alias>token could score any dataset of the project bydataset://reference (and probe which exist through…/validate) without dataset read access. Callers without it now send inline data or name the endpoint's own fit dataset; any other reference, existing or not, gets the same403 FORBIDDEN. - server: a GDPR export whose build process died stayed
runningforever and blocked every later export of the tenant. A build now holds a lease (MAPI_EXPORT_BUILD_TIMEOUT_S, after which it fails, plus five minutes); arunningexport past its lease is built once more, then failed, and only the build holding the lease may mark it ready. Metadata rows are capped atMAPI_EXPORT_MAX_METADATA_BYTES(tables cut short are listed underskipped), so the in-memory archive stays bounded. - server: an export still building when a tenant purge started wrote the whole archive
after the purge, where nothing would ever delete it. The deletion's start cancels the
tenant's queued and running exports (
revoked.exportsin the audit event), thequeuestore waits for a cancelled build to end (or its lease to pass), and a build that lost its lease never uploads and deletes an archive it uploaded after losing it. - server: a purge went on after
MAPI_DELETION_RUN_STOP_TIMEOUT_Swhile cancelled runs that never stopped still held signed upload policies undert/{tenant}/, so they could write objects after the object store was purged. Thequeuestore now also waits until those runs' signed links have expired (attempt start + run timeout + 30 minutes) and says until when. - server, web, client: GDPR exports could not be listed, and Settings > Data found the last
export only through the browser's
localStorage, so another admin or browser could neither see nor download an export that was running or finished (and could not start another one while it ran). NewGET /v1/tenants/{tenant_id}/exports(newest first, paged, tenant admins); Settings > Data shows the newest export from it, and the Python client hastenants.exports(tenant_id). - server: the
storage_bytesquota left out model bundles although the docs count them; a tenant's storage now adds the size of every model version's stored objects. - server: the per-tier egress cap (
egress_bytes_per_month, 5 GiB on the free tier) was defined but never measured. Every signed download link the API mints (dataset and model artifacts,ui/views, score and render outputs) and every public embed view (its inline data and PNG link) is now charged to the tenant's monthly egress, whichGET /v1/usagereports; past the cap the link is refused with429 QUOTA_EXCEEDED(withRetry-Afteruntil the month resets). GDPR exports are never charged; a purge deletes the counter. - server: the embeds' "platform recipes only" rule trusted the provenance in
plots.json, which the worker writes while it runs plugin code, so uploaded code could label any speccore.*and get it onto public embed links. Plots of model versions fitted with uploaded code (an overlay in their lineage) are saved withpluginprovenance, are never embeddable, and existing links stop serving them. - server, runner, spec: endpoint and model scores answered only with signed links to zarr
and Arrow files, so the Google Sheets card wrote the word "done" into
A1and the curl, R and TypeScript snippets printed links nobody could open without those libraries. Score runs now writesummary.json(the requested summary - mean and 90% HDI by default - as plain tables: coordinate columns, then one column per statistic, at most 100,000 cells), andScoreCallResult.summarycarries it inline once the run succeeded. The Apps Script builds its payload from anInputsheet (long format) when there is one and writes the summary tables to aResultsheet; the other snippets print the tables. - web, plot-sandbox, docs: embed links were advertised for Slack, but Slack does not frame pages and the embed page had no unfurl tags, so a pasted link showed a bare "Shared plot". The page now carries static Open Graph tags (a "Shared chart" card, never the plot's name or the token), and the UI and guides recommend posting the rendered PNG to Slack instead of promising a live chart there.
- docs: webhooks do not attach rendered plots, a deliberate deviation from the plan
(bodies carry ids and states only, so no chart or signed link reaches a third-party
receiver); the webhooks guide says so and shows how a receiver renders a chart with
POST /v1/plots:renderwhenmodel.version.readyarrives.
Fixed (M3 review: web)¶
- web: a new self-service sign-up landed in Expert mode, with no way into Guided mode but
the header's Mode switch. Until a user chooses, a
publictier workspace now starts in Guided mode (other workspaces and platform operators in Expert; the browser's stored v2 default is forgotten, per-user choices are kept), "Sign in" after the e-mail link opens/guided,/opens the Guided start page in Guided mode, and Expert navigation (g g) and Home (Start a guided analysis) lead to Guided. - web: the admin console's Promotions tab was a placeholder. It lists the endpoint promotions that needed attention across tenants (gate overrides with their reason and failing checks, blocked promotions and those refused for want of an override), from the audit log. There is still no queue: the gate decides each promotion at once.
- web: a project could be locked to Guided mode only through the API. Project admins now
have Lock … to Guided mode under Settings > Mode (
PATCH …/projects/{project_id}, audited as before). - web: a webhook's Send test button only replayed the newest delivery (and was disabled until one existed). It is now Resend newest, and the empty delivery log says how a new webhook gets its first delivery.
- web: the beliefs preview charted every channel, the unticked ones at the model's wide default prior, so they looked like the analyst's beliefs (ROI near 100, media shares adding up past 100 %) and squashed the ranges actually stated. The charts show the ticked channels only and name the others ("No view on …: … learns them from the data"). The mock engine previews every channel, as the server does.
- spec, web: a "Results without any media" belief stated without any channel belief
was silently dropped (
BusinessPriors.channelsneeded at least one entry, and the page then saved "no business priors").BusinessPriorsnow needs at least one belief of either kind (channelsdefaults to{}; a baseline-only block validates, and the "largest shares" warning no longer fires without channel shares), both MMM plugins translate a baseline alone, and the Guided page previews and saves it. - plot-sandbox: in dark mode, rule and text marks without a colour of their own were drawn black on the dark card (Vega-Lite's default): the beliefs preview's 90% line, the forecast's split line and its "Forecast starts" label. The theme colours rules in muted ink and text and ticks in body ink, so a median tick also stands apart from its bar.
- web: the Guided forecast panel always said "weeks" (daily data too) and accepted
horizons up to the template's limit (520), past the horizon a weekly forecast was fitted
for, which then failed. It names the data's period (days, weeks, months) and stops at the
fitted horizon (
max_extension), saying a longer one needs a new fit. - web: Guided fan charts (forecast, prior predictive) started their y axis at zero, which
squeezed the bands into slivers; the axis now fits the data (
zero: false). - web: the endpoint page said "Promoted by usr_01…" and its history listed user ids, and saved plots said "of mdl_01…". People are named from the project's members (the id stays in the tooltip, and an unknown id is shown as is), and saved plots name their model.
- web: sign-up asked people to accept the terms of service and the acceptable-use policy
without a way to read them. Both are links (new tab) to
VITE_TERMS_URLandVITE_ACCEPTABLE_USE_URL, by default the plain-language summaries the app now publishes at/termsand/acceptable-use. - web: the Guided upload box described weekly marketing data by market (and the MFF and Meridian presets) for every template, Customer choice and Demand by store included. It describes the rows each template needs and names the presets for the marketing mix only.
- web: adding an experiment showed a red error under every field at once, its example values ("-120000") read as prefilled data, and nothing said why Preview and Save were disabled. A field's message now waits until the field is left, the examples read "e.g. -120,000", and a summary beside the actions says what each unfinished experiment still needs.
Fixed (integration)¶
- docs: the Google Sheets tutorial showed the Apps Script from before score answers
carried their summary (it wrote
doneintoA1). Its snippet is the server's current script again (the summary tables in a Result sheet, an optional Input sheet), the tutorial says what the sheet shows, and the Node test checks the Result sheet. - spec: the example of
PlotEmbedCreated(in the OpenAPI document) used an/e/<token>link; the server answers with/embed/<token>. - docs: M3 verification and the landing page give the unaided-tester timing measured on
the integrated branch, with the script starting at
/(135 s, was 137 s from/guided).
Known gaps¶
- The M3 exit criterion is not met yet. Five non-technical testers must go from upload to report to scenario unaided in under 30 minutes. The protocol and observation sheet are written (the M3 verification page in the operations docs), and a script clicks through the same flow in 135 s of system time, but no tester session has run.
- Nothing has run against real Google Cloud or a Docker daemon. The Cloud Armor policy, the
trusted-proxy ranges Terraform sets for Cloud Run behind the load balancer, the Cloud Run
executor, the docker and docker-gvisor executors and the compose stack are checked with
fakes, mocked providers and
terraform validateonly. After the first deploy, check that sign-ups from two networks get different fingerprints. - Static rendering covers Vega-Lite only out of the box. Plotly needs kaleido and a system
Chrome, so
POST /v1/plots:renderrefuses it withMODE_NOT_SUPPORTEDunlessMAPI_RENDER_PLOTLY_ENABLED, and that path has not run in an image. Webhook bodies never carry charts (a deliberate deviation), and Slack shows one generic card for every embed link. - Webhooks have no synthetic test event: Resend newest replays the newest delivery, and a new webhook gets its first delivery from a real event.
- The Google Sheets Apps Scripts and the embed page inside Notion, Looker Studio and Power BI are tested only against stand-ins, never in the real tools.
- Egress is estimated from object size when a link is minted, not measured as bytes served; a caller that polls a score result is charged again on each poll. The GDPR export archive is still built in memory (bounded by two settings) because the object store has no streaming upload.
- There is no combined cap across all free-tier tenants beyond the global quota scope; the
Python client's Atlas
RemoteModelcaps draws at 300 on every tier (the server allows 500 on paid tiers). - The Guided/Expert choice is stored per browser, not per user on the server; there is no
mapicommand to lock a project to Guided mode (the web switch and the API do it). - In the
geo_mmmtemplate, the default priors of channels without a stated belief can still imply media shares above 100% in total; the beliefs preview no longer presents them as the analyst's beliefs. - The
/termsand/acceptable-usepages are plain-language drafts, not reviewed legal text. A public installation should publish its own and setVITE_TERMS_URLandVITE_ACCEPTABLE_USE_URL. - Some screenshots predate the M3 review's wording (the Guided beliefs page, the Integrations hub pages including the Sheets card, Settings).
- A few tests are timing-sensitive under a fully loaded laptop (the 100-fit Cloud Run load test, the Postgres-restart chaos test, Terraform's provider plugin start-up); each passes when run again.
[0.3.0] — M2: cloud & multi-tenant core — 2026-10-15¶
The platform is now multi-tenant and ready for Google Cloud. Many teams can share one deployment: each tenant has its own projects, members, API tokens, quotas, cost and audit trail, and PostgreSQL row-level security keeps their data apart. Runs can execute as Cloud Run Jobs, and Terraform describes every Google Cloud project. Nothing has been applied to real Google Cloud yet: cloud code is tested against fakes, and the Terraform is validated and tested against mocked providers. In summary:
- tenancy and auth: tenants with trust tiers and data residency, projects with ordered
roles (viewer < editor < runner < admin), browser sign-in through a backend-for-frontend
OIDC flow (with a dev identity provider for local work),
mapi loginfor the CLI, and checksummed, scoped, project-bound API tokens. - isolation: forced row-level security on every tenant table. The API runs as roles
without the bypass (
mapi_appfor tenant data,mapi_gatewayfor sign-in and operator lookups); only the orchestrator holds the platform role. The audit log and the cost ledger are append-only. - quotas and cost: token, tenant and global quotas with weighted fair-share leasing and 429 back-off; an integer-micros cost ledger with per-run cost, usage reports and cost by tenant for operators; a global spend cap.
- operations: kill switches (pause or cancel uploaded code, the spend cap), executors that operators can disable, a shared-tier gate that needs recorded conformance and security-suite evidence, audit events for every change, Prometheus metrics, dashboards, alerts and runbooks, plus a security probe suite and chaos tests.
- cloud: a Cloud Run Jobs executor (orphan adoption, fan-out, 429 handling), Pub/Sub push events, a Cloud KMS bundle signer, Terraform for the control, data and exec projects, and deploy and rollback scripts.
- web, CLI and client: tenant switcher, members, tokens, usage and an operator console
(tenants, quotas, cost, kill switches, executors, audit);
mapi admin,mapi tokensandmapi usage; client methods for every new endpoint. - docs: Administration and Authentication guides with tested walkthroughs, a security model, deployment to Google Cloud with the [verify M0] checklist, and reference tables generated from the code.
Every workspace package and both web apps are versioned 0.3.0. Contract v0.1 gains only additive changes. Upgrading from 0.2 runs migrations 0003 to 0006, which adopt existing rows into a platform tenant.
Added¶
- spec: the M2 contracts. They cover tenants, trust tiers and placement, ordered project
roles (viewer < editor < runner < admin) with one permission matrix, and
AuthContextas the single authorization decision. They also add checksummed API tokens stored as HMACs, scopes, BFF OIDC helpers (PKCE, cookies, safereturn_to) and Pub/Sub push claims; token/tenant/global quotas with fair-share leasing and 429 back-off; integer-micros cost pricing and the ledger (RunCostmoves tomapi_spec.cost); audit events; kill switches and executor health; and the 12-case executor conformance suite as data.mapi_spec.apigains the 40 M2 endpoints andROUTE_ACCESS, the scope and permission of every operation (all 44 M1 ones included). Contract v0.1 gains only additive changes. - server: authentication on every route.
- Bearer API tokens, browser sessions through an OIDC backend-for-frontend (authorization
code + PKCE, HttpOnly SameSite=Strict cookie, CSRF token), and ID tokens of the
mapiCLI (MAPI_OIDC_CLI_CLIENT_IDS). - A credential-less local administrator in the single-tenant profile, and a
devprofile with a built-in identity provider and seeded accounts. - Tenants, projects, members and invitations.
- Row-level security:
FORCE ROW LEVEL SECURITYon every tenant table,set_config(…, true)per transaction, failing closed, with separate app and platform roles. - Quotas and rate limits at admission and at dispatch.
- A cost ledger written whenever an attempt ends, for every executor.
- The audit log, the operator endpoints (tenants, quota policies, kill switches, executor health, audit) and usage reports.
- server: the
cloudrun-jobsexecutor for a fixed inventory of 3 bases × S/M/L. - Env-only overrides, orphan adoption by nonce, and a jobs.run token bucket with a shared 429 cool-down.
- Exit-code classification (OOM, timeout, partial) and
taskCountfan-out plumbing. - Tested against a deterministic
FakeCloudRun. - Pub/Sub push authentication: a Google-signed OIDC token, a JWKS cache, a subscription allow-list and decoding of the log entry.
- A Cloud KMS manifest signer, alongside the local one.
MAPI_OBJECT_STORE=gcs: Cloud Storage with keyless IAMsignBlobURLs and integrity checks on every write.- server: observability.
- structlog JSON logs carrying the trace and span ids.
- OpenTelemetry spans, with an optional OTLP exporter.
- The Prometheus metric set at
GET /metrics, behind a bearer (MAPI_METRICS_TOKEN, which the multi-tenant profile requires), plus the worker's own endpoint (MAPI_WORKER_METRICS_PORT). - Request ids on every response.
- tests: the executor conformance suite, run against fake Cloud Run and local-process. It also checks that every attempt that ran is in the cost ledger.
- A 100-fit load test on the fake backend (
make load-test). - Chaos tests (
make chaos). - A route-table test that every non-public route needs credentials and that no M1 or M2 route serves another tenant's project.
- End-to-end M2 suites against a running dev stack, run by
make e2e. Two tenants are created through the dev IdP. The suites cover sign-in, cross-tenant refusal at the API and by row-level security in Postgres, token scopes,QUOTA_EXCEEDED, the spend-cap kill switch held at dispatch, per-run cost and the audit trail (tests/e2e/test_m2_tenancy_stack.py). They also include a browser flow: sign-in, tenant switch, fit with metered cost, tokens, and the admin kill switch (apps/web/e2e/m2-flow.e2e.spec.ts). - An opt-in burst of concurrent real fits, against the dev stack or a deployment
(
MAPI_E2E_BURST_RUNS). - A nightly workflow: chaos, executor conformance, both load-test modes, and the security suite on local-process and inside gVisor.
- infra: Terraform modules control, exec, data, build, perimeter and stack, plus the dev, staging and prod environments.
- The exec projects get zero egress, a restricted VIP, a DNS response policy and VPC-SC.
- Cloud SQL with three login roles, generated secrets written only through write-only arguments, a Pub/Sub push to an internal-only events service, and KMS.
- Deploy scripts (
deploy/gcp) and a manual Deploy workflow. make tf-validateruns fmt, validate and the mocked-provider module tests; nothing is ever applied.- ops and security: Grafana and Cloud Monitoring dashboards, alert rules and runbooks
(
ops/). A sandbox-escape probe suite with an expected-outcome matrix per isolation level, and a deliberately malicious plugin that is never installed (security/, run withmake security-suite). - web: sign-in, a tenant and project switcher, and pages for API tokens, members, and usage and cost. Per-run cost, and plain-language quota and permission errors with a Retry-After countdown. An operator console for tenants, quota policies, kill switches, executor health and the audit log.
- client and cli:
mapi login(browser or device code, refreshed and stored with mode 0600, or--with-token),logout,whoamianduse, plusX-Mapi-Projectselection.- Token, usage and admin commands and resources.
- 429 handling with
RateLimited/QuotaExceededand Retry-After. --discovery-urlfor providers whose discovery document lives elsewhere (the dev IdP).mapi_client.oidc.dev_idp_loginfor scripts.- docs: tenancy and auth, and quotas and cost concepts; the v0.3 HTTP API; the authentication, Cloud Run, Google Cloud deployment, security hardening, observability and security-suite operations pages; and every new variable in the configuration reference.
- The M2 verification record (
docs/operations/m2-verification.md): what was proven locally, the measured numbers, and the exact steps left for a real Google Cloud project. - server:
GET /v1/admin/usage(operators) returns cost by tenant for a UTC day or month, summed from the cost ledger with the biggest spender first. It also returns the current spend against the global spend cap (AdminUsageReport,TenantCostandGlobalSpendinmapi_spec.api). The Python client exposes it asadmin.usage(). - web: the admin console has a Cost tab with cost by tenant per period and each tenant's share of spend. The global spend-cap card shows current spend against the cap on a meter that warns at 70% and 90%.
- docs: the v0.3 site.
- New Administration and Authentication guides, each driven by a walkthrough that
make e2eruns against a dev stack (docs/snippets/admin_tour.sh,auth_tour.sh). - Generated from the code at every build (
scripts/gen_docs_platform.py): the roles, token-scope and trust-tier matrices (mapi_spec.tenancy.PERMISSIONS), the metric set, the alerts, dashboards and runbooks ofops/, and an index of everyMAPI_*variable. Pages embed generated tables with<!-- generated: NAME -->. - The landing page tells the multi-tenant story, with screenshots of the new screens
(
make docs-screenshots-admin); the security model shows what each executor enforces and the threat model in plain terms; Deploying to Google Cloud starts with the [verify M0] checklist and ends at the first tenant; the roadmap moves to M3.
Changed¶
- devops:
make devand compose run the servers in thedevprofile by default (MAPI_STACK_PROFILE): sign in asadmin@dev.local(tenant admin and operator) or one of the other seeded accounts.make seed,make e2eandmapi_conformance run --stacksign in by themselves.MAPI_STACK_PROFILE=single-tenantrestores the credential-less API. - server: every v0.2 route is project-scoped. Callers name the project with
X-Mapi-Project: <tenant>/<project>, a token's binding, or the single-tenant default. - web: mutations send the session's CSRF token (
X-CSRF-Token).openapi.jsonand the TypeScript types are generated from the server alone. - client: QUOTA_EXCEEDED is no longer retried by default (
RetryPolicy.max_quota_wait). RATE_LIMITED has its own retry budget and is retried for every method.
Fixed¶
- server: the start-up check that every route has an access rule now covers routes of included routers (FastAPI 0.143 keeps them as branches).
- server: two finalizations of the same model at once no longer deadlock, and a retried finalization reuses its stored signed manifest.
- server:
/auth/callbacksends the browser back to the web origin, and a single-tenant server answers/auth/sessionas the local administrator. - web: costs below $0.0001 keep two significant digits instead of showing
$0.00. - Usage lists session runs as "Signed-in users" (the server's group key is
-). - Memory under an hour is shown in GiB-seconds.
- A finished run without output no longer says "yet".
- The project switcher no longer truncates names on wide screens.
- web: a live log tail no longer trips the tenant's rate limit. Before, it sent up to 10
requests every 500 ms while catching up. It now fetches one page per poll, at least 2 s
apart (5 s once caught up), and waits out
Retry-Afteron 429. - security: the docker and gVisor probe container gets the suite through a read-only mount; before, every isolation probe ended in ERROR on a host with Docker.
--executor cloudrefuses to run outside a Cloud Run Jobs execution.
Fixed (M2 review: spec, server and auth)¶
- server: an API token can no longer administer its tenant. Tokens are bound to one
project, so tenant members, project create and delete, the tenant audit log and tenant usage
now need a signed-in session (
403for any token). A token may still read its own tenant and list its own project (mapi_spec.api.TOKEN_TENANT_ROUTES). - server: signing in again no longer re-activates a disabled account.
/auth/callbackrefuses it with the generic401and auditsauth.login_failed; only an invitation becomes active at its first sign-in. - server: operator status is decided on every request from
MAPI_OPERATOR_EMAILS(and the dev profile's first account). Before, a demoted operator kept operator powers throughmapi loginID tokens indefinitely and through browser sessions until they expired. - spec/server: an API token can no longer mint tokens, whatever its scopes
(
mapi_spec.tokens.TOKEN_NEVER). A child token dropped its parent's expiry, vCPU budget and rate limit, and outlived its revocation.POST /v1/tokensneeds a signed-in session. - spec/server:
score:<target>tokens can score. They authorized nothing, because:scorechecksrunon the model and the scope covered only endpoints, so integrations fell back to broaderruns:createtokens.score:<model id>now covers:scoreon that model andscore:*on every model of the project (RouteAccess.name_param). The web tokens page, the CLI help and the docs say so. - server: concurrent uploads can no longer overrun the storage quota. Storage admission now locks the tenant row like run admission does, so six concurrent 800-byte intents against a 1000-byte quota admit exactly one.
- server: abandoned upload intents no longer hold storage quota forever. Usage counts a
pending intent only while its signed PUT is valid, and a new housekeeper loop in the worker
(
MAPI_HOUSEKEEPING_INTERVAL_S, default 300 s) marks expired intentsexpiredand deletes whatever reached quarantine. - server: expired browser sessions are deleted. The delete in
load_sessionwas rolled back with the request's 401, and nothing else removedweb_sessionsrows. The row is now deleted in its own transaction after the 401, and the housekeeper sweeps expired sessions and abandoned sign-ins. - server: a FINALIZING run no longer counts its vCPU estimate on top of its metered
attempt. Quota usage counts an active run's estimate only while its current attempt has no
actualledger entry, so runs near a daily or monthly limit are not refused by mistake. - server: concurrent submissions from different tenants can no longer exceed a global
concurrent_runsor vCPU limit. Run admission takes a transaction-scoped advisory lock after the tenant row when the global policy sets such a limit. - server: the global spend cap counts work in flight. The ledger only grows when runs finish, so every queued run used to be admitted and dispatched against the same figure. Admission now adds the estimates of admitted, unmetered runs, and the dispatcher adds the estimates of started runs and of each run it leases in the same pass.
- server: held runs can no longer fill the dispatcher's window of queued runs and stall
fair share for every tenant. Runs of suspended tenants, uploaded code paused by the kill
switch and non-internal work past the spend cap are left out in SQL, and each tenant takes
at most
MAPI_LEASE_CANDIDATES_PER_TENANT(50) of the window. - server: a retry resumes from the newest checkpoints of any earlier attempt. Before, it
only looked at the attempt just before it, so an attempt that died before its first segment
threw away all earlier progress. Automatic infra retries are also capped by cost
(
MAPI_AUTO_RETRY_COST_FACTOR, default 2× the estimate), as plan §8.3 asks, and no longer by count alone. - server: an unknown token prefix and a wrong secret for a known prefix get the same
answer (
the API token is invalid), so the API no longer tells which prefixes exist. Unauthenticated failures that anyone can cause (token.auth_failed,auth.login_failed) are audited at most five times per token prefix or client and minute, so they can no longer flood a tenant's audit log. The housekeeper sweeps old rate counters. - server/web: run event streams re-check the caller's credential every
MAPI_SSE_REAUTH_S(60 s). A revoked token, an ended session or a lost membership no longer keeps receiving a 24-hour run's progress and logs: the stream closes and the reconnect is refused. The web client stops on 401 (and raises the session-expired prompt) or 403, where it used to retry forever. - server: the audit log and the cost ledger are append-only (migration
0004_append_only_records). Both runtime roles loseUPDATEandDELETEon them, and a trigger refuses any change to an existing row, so a request-path bug or injection can no longer rewrite a tenant's audit trail or billing history. - server/infra: the default data residency follows the deployment. It used to be
useverywhere, so on the documented EU deployment every run of the platform tenant and of tenants created with defaults failed asINFRA_ERROR.MAPI_DEFAULT_RESIDENCYdefaults to the multi-region of the Cloud Run region, and Terraform sets it from the dispatch pool's region (default_residencyoverrides it). It applies to the platform tenant, to rows the tenancy migration adopts and toPOST /v1/admin/tenantswithoutdata_residency. Submissions that no reporting executor can place are refused at admission with a clear403instead of being queued, and the worker warns at start-up about such tenants. - server/infra: the API and events services no longer hold the RLS-bypassing database
role, which plan §9 reserves for the dispatcher, reconciler and finalizer. Their platform
lookups (sign-in, memberships, operator metadata, routing pushed events) now run as a new
mapi_gatewayrole (migration0005_gateway_role,MAPI_DATABASE_GATEWAY_URL,MAPI_DB_GATEWAY_ROLE). It has noBYPASSRLS, gets explicit policies on identity tables and column privileges on a few run and ledger metadata columns, and cannot read datasets, specs, models, plots, attempts, events or logs. Every API request uses it automatically. Pushed events are written through tenant sessions. Terraform adds themapi_gatewaylogin and itsdb-url-gatewaysecret, and only the orchestrator can readdb-url-orchestrator.
Fixed (M2 review: executors, cloud and infrastructure)¶
- infra: the deployed stack can run work at all. Terraform creates a create-only
url-uploaderaccount (MAPI_URL_UPLOADER), without which every run failed at dispatch; the server refusesMAPI_OBJECT_STORE=gcswithout a distinct uploader. - infra: checkpoint policies may overwrite their own checkpoints (conditional
storage.objects.deleteunder.../checkpoints/only), so PARTIAL bundles and resume see every segment on real GCS. - server: verified and public tenants are placed only on an executor with passing,
unexpired conformance and security-suite reports for its current caps
(
PUT /v1/admin/executors/{name}/evidence,MAPI_EXECUTOR_EVIDENCE_MAX_AGE_S); the admin console, CLI and web show the real state. Runs that no executor can place are refused at submission (503) instead of failing at lease, and the docs say that verified tenants need DNS enforcement verified first. - server:
docker-gvisorcan qualify for shared tiers on a network declared isolated (MAPI_DOCKER_NETWORK_ISOLATED). - server: Cloud Run 429s requeue a run without using up an infrastructure retry, leases
are renewed while a batch is submitted (for at most ten lease lengths per submit), and
Retry-Afteris capped. The executor's per-process bookkeeping is bounded. - sdk: zip artifacts (skops) are no longer rejected as pickles depending on the second they were written.
- infra: user log and progress events are excluded from the exec projects'
_Defaultlog bucket; the API identity only reads the quarantine bucket. - deploy: the deploy workflow is valid (no
runnercontext in job env; CI runs actionlint);rollback.sh --previouspicks the release before the deployed one (records are named after their images, rollbacks are marked); dry runs no longer claim what they did not do.
Fixed (M2 review: CLI, operations and docs)¶
- server: incident containment can now stop running tenant code.
PUT /v1/admin/executors/{name}(auditedexecutor.update, reason required;mapi admin executors disable|enable, a Disable button in Admin → Executors) disables an executor: orchestrators lease nothing onto it and their periodic reports no longer resetenabledto true. The kill-switch update takescancel_untrusted_runs(withuntrusted_execution_enabled=false;mapi admin kill-switches set --cancel-untrusted-runs, "Cancel running uploaded code" in the web app), which moves every unfinished run of uploaded code tocancelling. The sandbox-escape and incident-response runbooks use both. - ops: the jobs.run 429 alarm (plan §8.3) watches Cloud Run's 429s. The orchestrator
exports
mapi_executor_submit_throttled_total{executor}(every 429, retried ones included), andMapiJobsRun429and the dashboard use it instead of the API's own 429s to tenants onPOST /v1/runs. The Cloud Monitoring policy usesserviceruntime.googleapis.com/api/request_count(RunJob,response_code=429). Its old filter named a metric that Cloud Run jobs do not export, and it counted every 4xx. - ops: the plan's alert on running executions at 70 % of Cloud Run's 1,000 (§8.8)
exists. The orchestrator's report exports
mapi_executor_running_executions{executor}from the run table;MapiRunningExecutions70and a Cloud Monitoring policy onrun.googleapis.com/job/running_executionsfire above 700. - infra: on GCP the
mapi_*series reach Cloud Monitoring. - The orchestrator serves its own
/metricson127.0.0.1:9464. Before, the dispatch, reconcile and finalize series had no endpoint in the cloud. - api, events and the orchestrator run a collector sidecar (
metrics_collector_image, an OpenTelemetry Collector pinned by digest). It scrapes/metricsover loopback with the metrics bearer and writes to Managed Service for Prometheus. - Without an image Terraform warns that the alerts and dashboard stay empty.
- The nightly security jobs push
mapi_security_probe_unexpectedto the Pushgateway atMAPI_PUSHGATEWAY_URL. gcp-deployment.mdhas a Metrics section.- server: rotating the local bundle-signing key no longer breaks every existing model.
MAPI_SIGNING_TRUSTED_KEY_PATHSlists retired keys (public or private PEM) that still verify. Terraform gainscontrol.kms_trusted_key_versions(MAPI_KMS_TRUSTED_KEY_VERSIONSon every server workload). The key-rotation runbook now gives the real local and Cloud KMS procedures and the compromise path. It no longer tells GCP operators to rotate a Pub/Sub "internal token": pushes there are OIDC-authenticated. - docs: data residency is enforced only on executors that declare regions (Cloud Run Jobs). The self-hosted executors are not checked, and the tenancy page now says so instead of promising a region match everywhere.
- server:
MAPI_DISPATCH_TIERSrestricts an orchestrator to some trust tiers. Two orchestrators serving different exec pools can no longer lease each other's runs (leasing was not pool-aware, so public-tier code could land in the paid pool).gcp-deployment.mdno longer suggests a second orchestrator without it, and says that one environment deploys one pool until M5. - ops: three Prometheus rule fixes.
MapiQuota70is nowMapiQuotaRejections. It fires on rejections, not at 70 %, and it aggregatesby (scope), so its summary names the scope.MapiNoProgressIngestedalso fires when no event was ever accepted (or vector(0)).- Clearing the spend cap now resets
mapi_global_spend_microsandmapi_global_spend_cap_microsto 0, both at once and at every admission. The spend ratio ignores a cap of 0, soMapiGlobalSpendCap90stops firing. - cli:
mapi logout --revokerevokes the token in the project it is bound to (it was refused with 403 when another project was selected) and forgets the local credentials even when the revocation fails, then exits 1. - security: the report writers create their directory, so
make security-suite(and both nightly security jobs) no longer fail on a fresh checkout without.local/.
Fixed (integration)¶
- server: two review branches each added a database role for the API without the
row-level-security bypass. One is kept (
0005_gateway_role, column-level grants, used for every API request);MAPI_DB_PLATFORMfrom the other is gone. The executor-disable migration is renumbered0006_executor_disableand lets the gateway change an executor'senabledanddisabled_reason. - server: an executor's
enabledflag belongs to operators. Missing shared-tier evidence no longer turns it off; it shows inshared_tiers_enabledand the executor's message. - server: a submission that no executor can place gets one answer per cause. A residency
or resource class no executor serves is refused with
403. A trust tier that no executor can take yet (enforcement not verified, no suite evidence) gets503. - server: as the gateway role, the kill switch's
cancel_untrusted_runscancels each tenant's runs in a session of that tenant. The operators' usage report can read the ledger columns it sums. - tests: the end-to-end spend-cap drills use a self-hosted-trusted tenant, which the local executor can still place.
Known gaps¶
- Nothing has run against real Google Cloud. The Cloud Run, Pub/Sub, KMS and Cloud SQL code paths, the IAM conditions, the logging exclusion, the metrics collector sidecar and the Cloud Monitoring filters are checked with fakes and mocked providers only.
- No job posts the nightly conformance and security-suite results to the evidence endpoint
yet. Until an operator records them (and the M0 DNS check passes),
verifiedandpublictenants cannot run on any executor. - There is no endpoint to disable a user account.
disabledis set in the database, and sign-in keeps it. - The gateway role still reads some tables across tenants for the operator console, quota counters and sign-in.
- A live log tail polls the logs endpoint instead of using the run's event stream.
- The API's built-in
/docsand/redocpages load assets from a public CDN.
[0.2.0] — M1: local vertical slice — 2026-10-15¶
The platform now runs end to end on one machine. From the Expert web app (or the CLI, the Python client or plain HTTP) you can upload data, write and validate a spec, fit an E33-style hierarchical geo MMM, inspect it, and score it: an 8-week forecast, a new market, a weekly EUR plan and counterfactuals. Real Cloud Run and multi-tenancy come in M2 (v0.3.0). In summary:
- server (
mapi-server): a single-tenant FastAPI control plane on PostgreSQL with procrastinate. It includes a dispatcher, a level-triggered reconciler, a metadata-only finalizer with signed bundle manifests, HMAC-verified run events, SSE with resume, andlocal-processanddockerexecutors. It never imports plugin code or array libraries. - runner (
mapi-runner): a trusted supervisor and an untrusted worker harness for ingest, describe, validate, fit, score, evaluate, derive and render runs. It produces signed events, checkpoints, partial manifests and theui/views the web app reads. - gcs-shim (
mapi-gcs-shim): a local Cloud Storage emulator. It provides generations, server-side CRC32C/MD5, V4 signed URLs and POST policies, per-principal permissions and a write-once bundles bucket. - web and plot-sandbox: the Expert web app (YAML editor with live validation, runs with live progress and logs, model inspection, scoring) and a separate-origin, CSP-locked plot renderer.
- client and cli: a typed sync/async Python client for every v1 endpoint, and
mapifor local and remote work. - devops:
make dev(the whole stack without Docker), digest-pinned images, docker compose,make seed,make e2eandmake release-check. - conformance: the corpus also runs through the live stack (
run --stack URL), and a sklearn conformal fixture is added (7 fixtures in all). - docs: a 10-minute quickstart, guides to the web app, CLI, client and HTTP API, a deployment section, an architecture overview and a security model.
Every workspace package and both web apps are versioned 0.2.0. Contract v0.1 gains only additive changes.
Added¶
- spec: the M1 shared contracts, so the API, runner, executors, GCS shim, web app and plot sandbox can be built in parallel against one source of truth:
mapi_spec.run: the run state machine (RunState,RunTrigger, a puretransition()with owners, failure classes and attempt bumps), theRunSpecbootstrap object, typed event payloads, canonical HMAC-SHA256 event signing, the{"mapi_event": …}stdout format,OutputManifestwith metadata-only verification, the signedBundleManifest,RunnerEnvand exit codes.mapi_spec.storage:ObjectMeta, the asyncObjectStoreprotocol, signed GET/PUT and POST policy shapes, GCS-style CRC32C/MD5 helpers, storage errors and the bucket/key naming scheme.mapi_spec.api: REST DTOs for the v1 surface (each with examples), SSE/Prefer/idempotency helpers andM1_ENDPOINTS, the machine-readable endpoint table.mapi_spec.localdev: local ports, origins and environment variable names.- spec (additive, contract v0.1):
RunKindgains the fan-out parents (sweep,backtest,sbc,campaign);RunEventTypegainscheckpointandmetric;RunEvent.tsmust be timezone-aware and is normalised to UTC;PlotSpeccarries a schema example. The new schemas joinexport_schemas()and the contract snapshot. - docs: the "HTTP API (v1)" and "Run protocol" reference pages
(
docs/reference/http-api.md,docs/reference/run-protocol.md). - gcs-shim:
mapi-gcs-shim, a local Cloud Storage emulator. It serves the GCS JSON-API subset the platform uses, with generation and metageneration preconditions, plus V4 signed GET (withRange) and PUT URLs and V4 POST policies (key prefix and size range). It adds IAM-lite principals (control, andexecwith bootstrap get and delete only), a write-once bundles bucket, lifecycle sweeps, and server-computed CRC32C and MD5 on a crash-safe filesystem backend.ShimObjectStoreand the import-guardedGcsObjectStoreimplementmapi_spec.storage.ObjectStore, and apytest11plugin provides thegcs_shimfixture. Seedocs/reference/gcs-shim.md. - runner:
mapi-runner, the sandboxed run harness. A trusted supervisor reads and deletes the bootstrap object, starts the untrusted worker with a scrubbed environment (uid 10001 and no-new-privs as root, dev mode otherwise), signs every stdout event, caps logs, events, scratch and outputs, handles SIGTERM/deadline with partial manifests from checkpoints, and uploads outputs plus a signedOutputManifest. The worker runsingest(CSV/Parquet with a column mapping or the MFF preset, zarr zip, netCDF),describe,validate(dry run),fit(bundle, sidecars, plots,ui/views, checkpoints),score,evaluate,deriveandrenderthrough the SDK harness. Seedocs/reference/runner.md. - server: the M1 control plane (
mapi-server api|worker|migrate|dev): FastAPI/v1API covering everyM1_ENDPOINTSrow plus/v1/me, spec diff,ui/redirects and:evaluate; SQLAlchemy 2 + Alembic schema (every row carriestenant_id/project_id) with procrastinate on the same Postgres; dispatcher (token bucket, intent row before submit), level-triggered reconciler (executor status is the liveness source of truth, STALLED is a warning, orphan adoption by nonce), metadata-only finalizer with server-side copy and EC P-256 signed bundle manifests verified at URL minting; HMAC-verified event ingestion from the executor pipe andPOST /internal/events; SSE withLast-Event-IDresume over one LISTEN connection per process;local-processanddocker/docker-gvisorexecutors; OpenAPI 3.1 export (python -m mapi_server.openapi). - web:
apps/web, the Expert-mode web app (React 19, TanStack Router/Query/Table, Tailwind + shadcn/ui, Monaco + monaco-yaml with live:validatediagnostics, SSE run progress per chain, dataset upload via signed URLs, model inspection, scoring) and the Guided-mode shell; a pnpm workspace at the repo root. - plot-sandbox:
apps/plot-sandbox, the separate-origin Vega-Lite/Plotly renderer with a strict CSP and a validatedpostMessageprotocol. - devops: containers, local orchestration and developer tooling for M1:
images/:base-bayes(with aworkertarget),base-ml(no PyMC),base-forecast,server(control plane only, checked with import-linter and package assertions at build time),gcs-shimandweb(nginx serving the app and the plot sandbox on separate origins with strict CSPs). All are digest-pinned, run as non-root with a read-only root filesystem, and have health checks. The runner images bake the catalog JSON and a warmed PyTensor/Numba cache.docker-compose.yml(postgres 16, gcs-shim, migrate, api, worker with the local-process executor, web, plot-sandbox),compose.override.example.ymland.env.example, which documents everyMAPI_*variable.scripts/dev.py(make dev): runs the whole stack without Docker (pgserver PostgreSQL, GCS shim, migrations, api, worker and both Vite servers) with readiness probes, prefixed logs and clean process-group shutdown. Alsostop,status,migrate,db-shellandclean.- New make targets:
dev/run,stop,status,migrate,db-shell,seed(the E33 demo through the API),clean-local,docker-build,docker-pin,compose-up/down/logs,web-build,web-test,test-all,e2eandrelease-check. - CI: a web job, a build-only docker job for every image, and digest-pin and compose checks.
- Docs: "Local development" and "Containers and docker compose" (Operations), and
deploy/README.md. - client:
mapi-client, the typed Python client (Client,AsyncClient) for everyM1_ENDPOINTSrow: DTO-typed resources, RFC 7807 errors asMapiErrorsubclasses, idempotency keys and safe retries, lazy pagination, SSE withLast-Event-IDresume, dataset upload through signed URLs,Prefer: waitscoring, CRC32C-checked transfers without the API token. Seedocs/reference/cli.md. - cli:
mapi(new,check,fit,scorelocally or--remote,runs,datasets,catalog,plugin init|test,spec upgrade,serve,config), with--jsonoutput and ANSI-stripped untrusted text. - spec:
mapi_spec.jobs, the run job contract shared by the dispatcher and the runner (IngestJob,DescribeJob,ScoreJob,EvaluateJob,DeriveJob,RenderJob, input and output object names,MetricsSidecar,DATASET_SCHEMA_ATTR), andmapi_spec.views, the UI views the web app reads (mapi.preview/v1,mapi.profile/v1,mapi.tree-index/v1). - gcs-shim: CORS for the web origin (
--cors-origin, default$MAPI_WEB_ORIGIN) so browsers can use signed URLs, and awraphook onShimServer. - server:
mapi-server dev(and somapi serve) starts an embedded GCS shim when nothing listens atMAPI_GCS_ENDPOINT; the OpenAPI document publishes the run-event payload schemas. - docs: "CLI and Python client" and "Web app and plot sandbox" reference pages; the run protocol documents job payloads, object names and UI views.
- server:
GET /v1/runs/{run_id}/plots(list_run_plots) returns the plots a succeeded run rendered, sanitized on every read; the client hasruns.plots(). - web: the score form takes the input dataset and the spec's side tables (defaulting to the versions the fit used), shows the score run's own plots and pivots the summary by statistic.
- devops:
scripts/seed.py --export DIRwrites the E33 demo tables, score payloads and spec as files for the web app. - tests: the M1 exit criterion end to end against the running stack:
tests/e2e(Python client: fit, SSE, signed manifest and bundle generations, the bundle read back with the corpus' checks, five E33 score requests, bootstrap deletion, forged events) andapps/web/e2e/m1-flow.e2e.spec.ts(the same flow in the browser, plots in the sandbox iframe); a server test proves the finalizer refuses tampered output of the real runner. - docs: "Your first model in the web app" guide; the local-development page covers the
first start,
make e2eand the state under.local/. - docs: the M1 documentation set. A landing page with screenshots of the running platform
(
docs/assets/screenshots/, captured byscripts/docs_screenshots.mjsand shrunk byscripts/optimise_screenshots.py,make docs-screenshots); a validated "zero to first model in 10 minutes" quickstart; guides to every page of the Expert web app, the CLI, the Python client and the HTTP API; an OpenAPI explorer page (a vendored, hash-pinned Redoc bundle over the committedopenapi.json,scripts/vendor_redoc.py); a Deployment section (local, Docker Compose, container images, a configuration reference naming everyMAPI_*variable, troubleshooting); an architecture overview and a security model; an updated roadmap (M2 cloud and multi-tenant, M3 Guided mode).scripts/gen_docs_reference.py(run bymake docs-buildand the MkDocs hook) generates the CLI reference from the Typer app, the server settings table fromSettings, and copies the OpenAPI document.conformance/tests/test_docs_platform.pyruns the CLI walkthrough, checks the generated pages, the configuration reference, the Redoc pin and the screenshots, and (undermake e2e) runs the client, HTTP and remote-CLI walkthroughs against the live stack.
Changed¶
- docs: anchors are validated in strict builds; mermaid is pinned to 11.17.2 instead of
Material's moving
mermaid@11. - server: the object store is
ShimObjectStorebuilt from the shim'scontrolkey file (MAPI_GCS_ROOT, default$MAPI_DATA_DIR/gcs, orMAPI_GCS_KEY_FILE); local executions get theexectoken from the same directory unlessMAPI_EXEC_TOKENis set. RunSpec payloads and input names followmapi_spec.jobs(score data underdataset, side tables underside.<name>,bundle.manifestpassed to score runs);evaluate/derivedefault to the fit dataset andrendergets the bundle'splots.json.metrics.jsonis read asMetricsSidecar. - runner: fit writes
coords.jsonand ingest writessummary.json(the control plane's CoordPlan metadata);preview.json,profile.jsonandui/tree_index.jsonfollowmapi_spec.views. The runner tests run against the real GCS shim. - web:
openapi.jsonandschema.d.tsare generated from the real server (apps/web/scripts/gen_openapi.py, checked in CI); the@e2especs can reuse a running stack on any ports. - devops:
make devstartsmapi-gcs-shim serve --root …with CORS and passes the resolved URLs to the Vite servers; compose mounts the shim volume read-only intoapiandworker(MAPI_GCS_ROOT) so they can load the principal keys. - sdk: zarr zips record their data-variable order (group attribute
_mapi_var_order, removed again on read). - devops:
make e2eruns the browser@e2esuite (it ran the mocked smoke suite).
Fixed¶
- spec: static score validation binds dataset names to canonical names at every
inputs_stage, as the runtime does (a weekly EUR plan entering atweeklywas refused). - server: the dry-run and evaluate endpoints declare their
Runresponse in OpenAPI. - seed:
make seedships E33's raw schema with the data, so the fit keepsmarket: new_levelsand new-market scoring works. - client: the run event stream decodes the live
progressandlogframes (sent without an id) asRunLiveFrame; it aborted on the first progress frame of a real fit. - server: model lineage records the side-table versions a fit read (it was always empty).
- sdk: variables come back from a zarr zip in their written order; plot recipes that
default to the first variable chose a random one on a bundle read back from storage
(
mmm.response_curvesfailed). The response-curve recipe also prefers variables with achanneldim. - plot-sandbox: composite Vega-Lite specs (concat, facet) rendered at zero width; they now keep their own size and scroll when wider than the frame.
- plots: the convergence table's columns no longer overlap; the fan chart draws and captions only the intervals a summary holds.
- web: a spec opened right after "Save spec" no longer shows unsaved changes (the editor used CRLF for models created empty on Windows user agents), so Fit is enabled; the run page's result column no longer overflows; result tables show dates as dates.
- devops:
make seedignores aSIZEexported by other toolchains (conda). - devops: the compose
apiandworkercontainers start. Themapi/serverandmapi/workerimages bakeuv.lockand setMAPI_UV_LOCK_PATH, so both resolve the same image digest. Before, neither could resolve one and both failed at start-up. - devops: compose keeps the bundle-signing key on a
keysvolume thatmigratewrites andapi/workermount read-only (MAPI_SIGNING_KEY_PATH,MAPI_SIGNING_KEY_CREATE=false). Each container used to mint its own key on its tmpfs, so the API rejected every bundle the worker signed, and a restart orphaned every model.mapi-server migratenow creates the key, and concurrent first starts converge on one key. - server:
mapi-server api|worker|devmake themselves non-dumpable on Linux, so local-process runs (same uid) cannot read their environment or memory. The worker's start-up warning and the operations docs state what such runs can still read. - server: a
Prefer: waitlong-poll stops waiting when its client disconnects. - client:
models.score()extends the read timeout of each long-poll by itswaitand treats a read timeout as "still running", so scores slower than about 90 s no longer raiseTransportError.stream_events()counts only connections that deliver nothing towardsmax_reconnects, so it follows long fits behind proxies that cut connections. - cli: run progress and state messages are never parsed as rich markup (no terminal
hyperlink injection, no
MarkupErrorabortingmapi fit). - devops:
.env.exampledocuments everymapi-serversetting, and the devops test now derives the list fromSettings.
Fixed (M1 review: spec, runner, shim and plots)¶
- runner: fit runs write
ui/preprocess.json(mapi_spec.views.PreprocessView,mapi.preprocess/v1): each step's resolved plugin, learned JSON and small numeric state arrays, so the web app's Preprocessing tab is no longer empty. - runner: an
INFRA_ERRORreported by the worker fails the run as a user error. Only failures the supervisor sees itself are retried automatically as infrastructure failures. - runner: every string the worker sends has ANSI, control and bidi characters stripped. The
CLI's
strip_controlalso removes the bidi ranges. - server, gcs-shim: the artifacts upload policy is create-only
(
post_policy(create_only=True)signsx-goog-if-generation-match: 0, and the shim answers 412 for an existing key). - plots: recipes draw from
mapi_plots.palette, whose dark variants keep at least 3:1 contrast, and the plot sandbox swaps them in dark mode. The decomposition legend is titled "component" when baseline or controls join the channels. Faceted fans and MMM curves give each panel its own axes, in coordinate order. - conformance: new
sklearn_store_demandfixture. Fixtures not yet ported are listed inDEFERREDwith reasons (list --deferred).run --stack URLdrives fixtures through the API.
Fixed (M1 review: server)¶
GET /v1/runs/{id}/plotsand score results are scoped to the caller's tenant and project.- Runner JSON is parsed with a nesting limit, and deep nesting is reported as
INTEGRITY_FAILURE. - Bundle inputs are verified again at dispatch and on
:retry. - The API refuses unexpected
Hostheaders (MAPI_ALLOWED_HOSTS), nginx answers onlyMAPI_WEB_SERVER_NAMESand the GCS shim checks hosts too, which guards against DNS rebinding. - Ingest and spec validation enforce the per-resource-class data caps (
PAYLOAD_TOO_LARGE). - An omitted fit seed is pinned to
0at submission, so lineage records the seed actually used. - Run lifecycle:
- A cancelled checkpointed fit ends
PARTIALand can be resumed (newCANCELLING --complete--> FINALIZINGedge). :cancelon a finalizing run is a no-op.- A run stopped at its deadline fails as
TIMEOUTor finalizes asPARTIAL. - An exit counts only after its event backlog is ingested.
- A restarted reconciler re-adopts live executions.
- Stalled finalize jobs are re-queued.
- Executor cancels no longer block the reconcile pass.
- SSE reads the run state before its events, and a run never shows another attempt's progress.
- Docker executor:
- Each plugin namespace gets its own versioned
mapi/base-*image (MAPI_DOCKER_IMAGES). - Run containers join the shim's network namespace;
noneis refused. - The supervisor and the worker run under separate uids (10001 for the worker), and
MAPI_REQUIRE_ISOLATION=1fails a run closed rather than falling back to dev mode. - The import-linter contract also forbids plugin packages, plot recipes and columnar libraries
in
mapi_server. - Docs: invalid submissions are refused without a run, and
REJECTEDis reserved.
Fixed (M1 review: web)¶
- Plots:
- The plot host accepts a sandbox
exportedreply only for an export the app requested. - Downloading a score output saves a checksum-verified file instead of leaving the app.
- Run pages:
- Live
progressandlogSSE frames, which the server sends without an id, are delivered. - The live log view follows the tail.
- Performance: Monaco and Arrow are no longer in first paint, and routes are lazy-loaded. A build-time budget keeps first-paint JavaScript under 640 KiB (it was 4.26 MB; now about 515 KB).
- Spec editor:
- It never replaces unsaved edits when a newer revision appears.
- Save sends the revision the text came from as
parent_revision. - Accessibility:
- No sideways scroll at phone width, and the compact nav shows every page.
- Status colours and form borders meet WCAG AA.
- Score form errors are marked on their fields and announced.
- The Score page resets its form and results when the version changes, and shows a loading state while the version loads.
Fixed (integration)¶
- tests: a devops docstring no longer names a fake
MAPI_variable that the configuration reference test picked up. - web:
openapi.jsonandschema.d.tsare regenerated (the cancel endpoint documents the finalizing no-op). - docs: pages written before the review fixes landed now describe the shared compose
signing key,
ui/preprocess.json, the docker executor's network and images, and the newMAPI_ALLOWED_HOSTS,MAPI_WEB_SERVER_NAMES,MAPI_DOCKER_IMAGES,MAPI_SIGNING_KEY_CREATEandMAPI_REQUIRE_ISOLATIONsettings.
Known gaps¶
- The docker executor, the compose stack and the new compose smoke test have not run against a real Docker daemon yet.
- Under the
local-processexecutor, run code shares the orchestrator's uid and can read its key files. The uid split is M2. MAPI_PROFILEmeans a config profile to the CLI and client, but the deployment profile to the server.- The API's built-in
/docsand/redocpages load assets from a public CDN. - Conformance fixtures E13, E84-lite, E82-slim and E45 are deferred: they need new plugins or run kinds.
[0.1.0] — M0: contract layer — 2026-10-15¶
First milestone release: the typed contract layer and the in-process xarray runtime, with no platform services yet. In summary:
- spec (
mapi-spec): pure-Pydantic contracts (DataSchema, SchemaEffect, ModelSpec, ScoreRequest, capabilities, representations, PlotSpec envelope, error codes) with static validation and JSON Schema export, frozen as contract v0.1. - sdk (
mapi-sdk): wire formats, the stage/split/context/replay/inverse pipeline engine, CoordPlan, bundles without pickle, thefit_spec/score_bundleharness, the in-process executor and themapi_sdk.testingcontract kit. - steps (
mapi-steps): 28 built-incore.*preprocessing steps and five split kinds, each with exported static effects. - pymc (
mapi-pymc) and sklearn (mapi-sklearn): model adapters where an author writes onlybuild()orestimator(). - plots (
mapi-plots): JSON-only Vega-Lite/Plotly sanitizer and thecore,mmmandmlrecipe catalogs. - plugins:
mmm.hierarchical_geo,mmm.pymc_marketing,ts.hsgp_trend,ts.gaussian_hmm,tabular.hierarchical_glmandtabular.regressor. - conformance (
mapi-conformance): the corpus runner and six fixtures (E02, C08, E24, store demand, E33, pymc-marketing), all passing at--size tiny. - docs: the MkDocs Material site with tested snippets and generated catalogs.
Every workspace package is versioned 0.1.0. The server, runner, CLI, client and GCS shim packages are still skeletons.
- Repository scaffold: uv workspace, package skeletons, split Apache-2.0 / AGPLv3+commercial licensing.
Contract v0.1 (frozen)¶
mapi_spec.CONTRACT_VERSION = "0.1"names the contract the exported JSON Schemas describe; every exported schema carries it asx-mapi-contract-version.- The contract is frozen for the v0.1.x line:
packages/spec/tests/snapshots/contract.jsonpins a digest of every exported schema and the drift test fails on any change. Additive changes (new optional fields, new error codes) regenerate the snapshot (MAPI_UPDATE_SNAPSHOTS=1); breaking changes bumpCONTRACT_VERSIONfirst.
Added¶
mapi-spec: the pure-Pydantic contract layer.- DataSchema, DimSpec and VarSpec, with schema templates.
- SchemaEffect propagation; ModelSpec, ScoreRequest and the Sweep, Backtest, SBC and Campaign wrappers.
- A strict YAML 1.2 loader, canonical JSON and spec hashes.
- Static validation: split stage,
LEAKAGE_RISK, dim policies, recursion and the CoordPlan checks. - Capabilities, representation rules, the PlotSpec envelope, stable RFC 7807 error codes, the Executor protocol shapes, JSON Schema export and the
mapi-specCLI. mapi-sdk: the xarray runtime.- Wire formats: XrJSON, single-object zarr v3 zips, DataTree zips and Arrow IPC.
- Array-level schema validation; the step contract, pipeline engine and stages.
- Split and per-group context replay; inverse chains with output-space labels.
- CoordPlan and coordinate canonicalisation, the representation-aware summariser and metrics.
- Bundle IO with integrity sidecars and no pickle; the
fit_spec/score_bundleharness withevaluate_then_refit. - The entry-point registry, the in-process executor, and the
mapi_sdk.testingcontract kit (check_step,check_model, pytest plugin). mapi-steps: 28 built-incore.*steps (reshape, value, feature, formula, adstock and saturation).- Each exports a templated SchemaEffect and a shipped catalog JSON.
- Catalog entries for the five split kinds.
mapi-pymc:PyMCModel, where an author writes onlybuild().BuildContextwithhier_effectnew levels.- Engines: nutpie, pymc, MAP and ADVI, plus guarded numpyro, blackjax and pathfinder.
- Segmented, resumable sampling with checkpoints; CRN counterfactuals.
- Convergence diagnostics, FrozenPredictor fast scoring and
check_fast_score. mapi-sklearn:SklearnModelwith an allow-listed estimator registry (sklearn and xgboost).- skops and xgboost-JSON persistence.
- Point, quantile, bootstrap, conformal and classifier plugins (
sklearn.tabular_*). - Pooled new levels; exact or zeroing contributions.
mapi-plots: a JSON-only PlotSpec sanitizer (pinned Vega-Lite 6.4.1 and plotly.js 4.1.1 schemas, an expression allow-list, HTML stripping) and role-bound templates.- The standard
core.*recipe catalog, plus themmm.*andml.*domain packs. - Altair and Plotly converters with named Arrow datasets, and the
render_plotsbatch entry point. mapi-plugin-mmm: marketing-mix model plugins (entry pointmapi.models: mmm).mmm.hierarchical_geo1.3.0, the E33 hierarchical geo MMM: adstock overcore.lag_tensorfeatures, logistic/Hill saturation, channel effects partially pooled across geos, new geos from the hyperprior, additive contributions.- Fit-cached derived quantities in original units: contributions, per-draw response-curve grids for Guided sliders, marginal cost/ROI; a decision objective for budgets; fast scoring.
mmm.pymc_marketing1.0.0, an adapter around pymc-marketing 1.2'sMMM(verified on PyMC 6.3) that owns scaling, carry-over and units and never pickles.- Declarative priors in pymc-marketing's dict form, typed per parameter so wrong priors fail static validation; a synthetic E33 simulator with known parameters and a parameter-recovery test.
mapi-sdk:outputs.derivedwithcache: fitis computed on the production fit through the plugin'sderivehook;ScoreContext.to_originalmaps model-stage arrays to the output space.- Cross-package conformance suites:
- the plan's xgboost spec through real steps, models and plots;
- an E33-shaped
core.*pipeline in front of a PyMC model. mapi-plugin-ts:ts.hsgp_trend(C08: HSGP trend + seasonality whose domain comes from the anticipated coords;HORIZON_EXCEEDS_FITpastmax_extension; sentinel months left out of the likelihood) andts.gaussian_hmm(E24: forward-algorithm HMM over padded tracks with aterminal_stategroup, forecast and filter modes, relabelling postprocess, LOO declared invalid).mapi-plugin-tabular:tabular.hierarchical_glm(E02: partially pooled GLM in long layout, hyperprior new groups, group-level covariates, four families) andtabular.regressor(plan §6.2 store demand: quantile regression restricted to the registry's quantile-capable estimators).mapi-conformance: the corpus runnerpython -m mapi_conformance run [--size tiny|full] [--fixture id](validate, fit, score requests, evaluate, contract kit, plots, expectations; summary table and JSON) with thee02,c08,e24,xgb_store_demand,e33andpymc_marketingfixtures;pytest -m conformance,make conformance(tiny) andmake conformance-full.- Score cases can opt out of side tables (
side_tables: false, e.g. a weekly EUR plan entering after the FX step). - Expectation hooks that declare
ctxreceive aFixtureContext(seed, size, registry, side tables and earlier score results). - Docs: spec format, SDK, built-in steps and plots references; guides for plugins, PyMC, tabular models and plots.
- Docs: built-in models reference, marketing-mix models guide, forecasting and sequential models guide, conformance corpus guide.
- Docs: the MkDocs Material site.
- A landing page, getting started (install, first model, runs and executors), concepts (labelled data, pipelines and stages, representations, bundles, coord plans, capabilities), the "port a model in 15 minutes" guide, licensing, contributing and roadmap pages for v0.2–v0.4.
- Generated reference pages: steps, models and plot-recipe catalogs, the ModelSpec/ScoreRequest schema and error codes (
scripts/gen_docs_catalog.py, run by an MkDocs hook andmake docs-catalog); API reference via mkdocstrings. - Every Python example lives in
docs/snippets/and is executed byconformance/tests/test_docs_snippets.py(make docs-test).
Changed¶
- The workspace runs numpy 2.4 (pymc-marketing 1.2 requires numpy < 2.5).
make typecheckalso coversplugins/mmm,plugins/ts,plugins/tabularandconformance.- Score requests that name a pinned side table now fail validation. Score outputs along time and extend
dims come back sorted.
validate_specemits an INFO issue when the model's target role overrides a declared role. - Plugin-raised error text now appears in
untrusted_text, not indetail.
Fixed (integration)¶
- The plan's xgboost example names the real quantile plugin (
sklearn.tabular_quantiles). A newml.feature_importancerecipe covers its plot list. core.fit_vs_actualandcore.residuals_over_timecompare quantile and interval summaries cell by cell instead of refusing them.- The contract kit flags step settings whose defaults are missing from the exported JSON Schema (
default_factory), and finds reverse-bound outputs when checking units. - Holdout metrics no longer invert the predictions of models that own
inverse_unitsa second time. - Score outputs that declare their own
units(e.g. state probabilities) no longer go through the target's inverse transforms. - The contract kit runs its shuffled, subset and new-level checks on long layouts (index variables are remapped) and shifts calendar-frequency time coords by whole periods.
- The in-process executor no longer resolves a score request's side-table refs when the run payload already supplies those tables.
- The contract kit's score requests name the spec's
score_policy: suppliedside tables, so models with supplied tables (E33's FX) pass the scoring checks. - The E33 and pymc-marketing fixtures use the corpus runner's fixture layout and run in
python -m mapi_conformance run.
Fixed (M0 review: spec and SDK)¶
- Counterfactuals:
score_bundletakes a baselinedatapayload and a separatescenariopayload, and scores the scenario's interventions against the baseline. A scenario without baseline data, or a named scenario that was not supplied, isSCHEMA_INVALID. ownsis enforced: catalog steps declareprovides(scaling,carryover), andvalidate_specrejects such a step on the target, media or spend variables of a model that owns that preprocessing (CAPABILITY_UNSUPPORTED).- Target dependence follows the model template's target roles, even with an inferred raw schema, so
RECURSIVE_UNSUPPORTEDfires reliably. The recursion and future-covariate checks measure how far the payload extends past the fit, not onlyhorizon. - No target leakage into evaluation lags for dynamic or cell-level splits.
as_ofgap periods stay in the holdout with masked targets and are excluded from metrics. - Pinned side tables always replay the fit-time version; a request that names one fails with
SIDE_TABLE_COVERAGE. - Score payloads are sorted along ordered/extend dims before the pipeline; duplicate labels are
COORD_NOT_ALLOWED. - Chained look-back steps (adstock then lag) replay with fit-time history. A score window that starts inside the fitted range and loses look-back history gets a WARNING.
- Long-layout per-group context works end to end.
- Inverses: steps declare
additiveinverses, and decompositions alongcomponentonly go through additive steps (otherwise they stay at the blocking stage, with a WARNING). Smearing residuals come only from target-shaped outputs. The momentssduses the delta method. - Model-owned outputs are labelled per variable with their units and
output_space. - Derived quantities named in
output.varscome back from the fit cache. An uncached one is refused. - PyMC CRN counterfactuals give identical draws in untouched cells for Poisson, negative binomial and
binomial likelihoods (
crn_ppf). - Wire and bundle hardening: XrJSON budgets decoded bytes (
max_decoded_bytes), caps declared dim sizes and accepts only ISO-8601 dates. Zarr-zip decoding is bounded by declared shapes, string lengths and an expansion ratio. Pickle is detected by opcode stream, in object.npyand inside archives. The evaluation sub-bundle is verified and never kept stale. Entry points can no longer replace registered or built-in plugin ids. - Spec parsing rejects non-finite floats and reports oversized JSON integers as
SpecParseError.
Fixed (M0 review: models and steps)¶
core.formula: scalar~is logical not; overflow and division by zero give inf/nan, and other arithmetic errors are reported asSCHEMA_INVALIDrather than escaping.- PyMC checkpoints: the codec rebuilds only classes defined in PyMC step-method modules (plus
pymc.utilandmapi_pymc), at exactly the encodedmodule:qualname.
Fixed (M0 review: plots, conformance and tooling)¶
- Plot sanitizer (Plotly):
updatemenus/slidersmethodargsare checked in the positional[attr, value]form and inside nested frame/trace objects, so external image sources,href/urlpaths, tile-map subplots and tile-map trace types can no longer hide there. - Plot sanitizer (Vega-Lite):
select.togglestrings andbind.legendevent filters are checked as expressions,event['view']is rejected likeevent.view, andusermeta.embedOptionsis rejected. from_plotlystrips HTML from (and scans) the strings it moves into Arrow datasets, which the JSON sanitizer never decodes.- Plugin-originated error text stays out of trusted fields:
render_plotsand the step/model wrappers (Pipeline, harness fit/score/derive) move a pluginMapiError's detail intountrusted_textand keep its code only when plugin code may raise it (mapi_spec.errors.from_plugin_error,PLUGIN_ERROR_CODES). - Contract kit,
check_step:effect_matches_outputalso compares each dim's kind and frequency and each variable's units; the toy FX and scale steps now set the units they declare. - Contract kit,
check_model: newhorizon_exceeds_fit,missing_future_covariate,recursive_unsupported,fast_score_matches_exact(plugincheck_fast_scorehook, provided byPyMCModel),plots_sanitizeandoutputs_match_manifestchecks;extend_typed_errorscovers integer extend dims;output_space_and_unitscovers outputs that inherit the target implicitly. OutputDecl.dims_of(additive) lets a manifest declare settings-dependent data dims; the sklearn models (and sotabular.regressor) andmmm.pymc_marketinguse it, so their static output dims match whatscorereturns.